In an Angular template, what is the difference between a property binding such as [value] and an attribute binding such as [attr.colspan]?
answer
- two different targets on one element
- inputs first, then the DOM property
- attr. means setAttribute
- null or undefined removes it
- checked against the DOM schema
basics
~20 s[value] writes the element's DOM property, or a matching component or directive input, and is checked at compile time. [attr.colspan] calls setAttribute with the stringified value and removes the attribute for null or undefined; use it when no matching property exists.
solid answer
~40 sA **property binding** `[name]="expr"` first looks for inputs named `name` on the component and directives of that element; if none match, it sets the **DOM property** `element.name`. The compiler checks the name against its DOM schema, so a typo or a missing property fails with `NG8002` ("Can't bind to 'x' since it isn't a known property of 'y'"). A handful of lowercase names are mapped for you: `class`, `for`, `formaction`, `readonly`, `tabindex`. An **attribute binding** `[attr.name]="expr"` skips all of that: it calls `setAttribute` with the value converted to a string, and `removeAttribute` when the value is `null` or `undefined`. You need `attr.` for attributes without a same-named property - `colspan` (the property is `colSpan`), SVG attributes, custom attributes - and when you really mean the attribute, not the live property.
code
html · 12 lines<!-- DOM property (or an input named 'disabled' if one matches) -->
<button type="button" [disabled]="isSaving()">Save</button>
<!-- mapped lowercase name: sets the tabIndex property -->
<div [tabindex]="focusable() ? 0 : -1">...</div>
<!-- no 'colspan' property exists: bind the attribute or the colSpan property -->
<td [attr.colspan]="span()">Total</td>
<td [colSpan]="span()">Total</td>
<!-- removed while rowId() is null -->
<tr [attr.data-row-id]="rowId()"></tr>go deeper
Know that [x] targets a DOM property or input, [attr.x] targets the HTML attribute, and null removes an attribute.
Explain the resolution order (inputs, then DOM property), the compile-time NG8002 check, the mapped names like tabindex, and why colspan needs attr. or colSpan.
Choose the target deliberately for accessibility and form state, and review templates for attr. bindings that stringify booleans into truthy attributes.
Encourage property bindings and typed inputs as the default contract, reserving attr. for cases the DOM schema cannot express.
## Two targets on the same element Every HTML element exists twice: as **markup attributes** (strings in the HTML) and as a **DOM object** with **properties** (typed JavaScript values). Many pairs share a name and stay in sync, but not all: the attribute `colspan` corresponds to the property `colSpan`, and on an `<input>` the `value` attribute is only the initial value while the `value` property is what the user sees now. Angular gives you explicit syntax for each target instead of guessing. | | `[name]="expr"` | `[attr.name]="expr"` | |---|---|---| | Target | component/directive input, else DOM property | the HTML attribute | | Value written | the raw value, any type | the value converted to a string | | `null` / `undefined` | assigned as is | attribute removed | | Compile-time check | name must be an input or a known DOM property (`NG8002`) | no property lookup | | Typical use | `[disabled]`, `[value]`, `[src]`, component inputs | `colspan`, SVG attributes, custom attributes | ## How Angular resolves a property binding For `[name]="expr"` the compiler and runtime work in this order: 1. **Inputs first.** If a component or directive on the element declares an input called `name` (or aliased to it), the value goes to every such input and the DOM is not touched. 2. **Otherwise the DOM property.** The runtime assigns `element.name = value`. A few lowercase attribute-style names are mapped to their property: `class` to `className`, `for` to `htmlFor`, `formaction` to `formAction`, `readonly` to `readOnly`, `tabindex` to `tabIndex`, and `innerHtml` to `innerHTML`. 3. **Compile-time validation.** Angular ships a DOM element schema. If `name` is neither a known property of that element nor an input of anything matched on it, the build fails with **`NG8002` Invalid Attribute**: "Can't bind to 'colspan' since it isn't a known property of 'td'." Interpolating inside an attribute value, `alt="Photo of {{ name() }}"`, is also a property binding of the resulting string. ## What an attribute binding does `[attr.name]="expr"` compiles to an attribute instruction: - a non-null value is converted with `String()`-like rules and written with `setAttribute`, so `false` becomes the text `"false"` and `2` becomes `"2"`; - `null` or `undefined` calls `removeAttribute`; - no input or DOM-property lookup happens, so any attribute name is accepted (event-handler attributes such as `onclick` are the exception, blocked for security). ## When you need attr. - **The attribute has no same-named property**: `colspan` and `rowspan` on table cells (`colSpan`/`rowSpan` are the properties), most SVG presentation attributes, and custom attributes such as `data-row-id`. - **You mean the attribute, not the live state**: `[attr.value]` changes an input's default value, not what the user typed. - **ARIA** used to be the classic case (`[attr.aria-label]`); since v20.2 `[aria-label]="..."` is accepted directly and also writes the attribute. ## Four spellings of one attribute The same name can appear in four forms, and each compiles differently: - `title="Help"` - a **static attribute**; it also initialises a matching input with the string. - `title="Help for {{ topic() }}"` - an **interpolated attribute**, compiled to a property binding of the concatenated string. - `[title]="hint()"` - a **property binding** carrying the raw value to an input or the DOM property. - `[attr.title]="hint()"` - an **attribute binding**, stringified, removed on `null` or `undefined`. ## A quick decision guide 1. The name is an **input** of the component or a directive on the element: use `[name]`. 2. The element has a **DOM property** with exactly that name (check the element's TypeScript type, such as `HTMLTableCellElement`): use `[name]`. 3. It is an **ARIA** attribute: use `[aria-name]` (v20.2+) or `[attr.aria-name]`. 4. It is an attribute **without a property** (lowercase `colspan`, SVG attributes, custom attributes), or `null` should mean "absent": use `[attr.name]`. ## Common mistakes - Writing `[colspan]` and being surprised by `NG8002` - use `[colSpan]` or `[attr.colspan]`. - Using `[attr.disabled]` for a boolean state; it writes `"false"`, which still disables the element. - Assuming a property binding on a component element also sets the DOM property of the same name - an input match consumes it.
- Why does [tabindex]="0" compile in an Angular template while [colspan]="2" does not?Angular keeps a small map of lowercase names to DOM properties, and `tabindex` is in it (mapped to `tabIndex`). `colspan` is not, and the element's property is `colSpan`, so the compiler finds neither an input nor a property and reports `NG8002`. Write `[colSpan]` or `[attr.colspan]`.
- What does [attr.value] change on an <input> compared with [value]?`[attr.value]` sets the `value` attribute, which only defines the initial or default value; once the user types, the visible text is the `value` property and the attribute no longer controls it. `[value]` writes the property, so it always changes what is shown.
saying these in an interview costs you the question
- Angular picks attribute or property automatically at runtime
- [attr.x] removes the attribute when the value is false
- [colspan] works because colspan is a valid HTML attribute
- A property binding sets both the input and the DOM property
- [attr.x] passes numbers and objects through without stringifying