In an Angular table header written as <th [colspan]="span()" [aria-sort]="sortDir()">, which binding fails to compile, and how should each be written?
answer
- one name has no property
- the property is camelCase
- ARIA names bind directly now
- null means no sort attribute
- an input can take the value
basics
~10 s[colspan] fails with NG8002 because a th has no colspan property; write [colSpan] or [attr.colspan]. [aria-sort] compiles since Angular 20.2 and writes the aria-sort attribute, removing it when sortDir() is null.
solid answer
~40 s`[colspan]` is a property binding, and the DOM property on a table cell is `colSpan`; Angular's schema has no `colspan` property and does not map that name, so the build fails with `NG8002`: "Can't bind to 'colspan' since it isn't a known property of 'th'". Use `[colSpan]="span()"` to set the numeric property, or `[attr.colspan]="span()"` to set the attribute. `[aria-sort]` is fine: since v20.2 a square-bracket binding whose name starts with `aria-` is compiled to an ARIA instruction that writes the **attribute** (or a directive input of that name, if one matches), and removes it for `null` or `undefined` - exactly what an unsorted column needs. `[attr.aria-sort]` is the older equivalent, and `[ariaSort]` binds the DOM's ARIA reflection property instead.
code
ts · 26 linesimport { Component, signal } from '@angular/core';
type SortDir = 'ascending' | 'descending' | null;
@Component({
selector: 'app-orders-table',
template: `
<table>
<thead>
<tr>
<th [colSpan]="span()" [aria-sort]="sortDir()" scope="colgroup">
<button type="button" (click)="toggleSort()">Amount</button>
</th>
</tr>
</thead>
</table>
`,
})
export class OrdersTable {
protected readonly span = signal(2);
protected readonly sortDir = signal<SortDir>(null);
protected toggleSort() {
this.sortDir.update((d) => (d === 'ascending' ? 'descending' : 'ascending'));
}
}go deeper
Remember that [colspan] fails and [colSpan] or [attr.colspan] works, and that ARIA attributes can be bound with [aria-*].
Explain NG8002 via the DOM schema, the property-name mapping exceptions, and how the ARIA instruction writes or removes the attribute.
Build accessible tables where only the sorted column carries aria-sort, and know when a directive input will consume an aria- binding.
Set accessibility conventions for data tables and grids, including which binding form the team uses for ARIA state and relationships.
## The scenario A data table has a grouped, sortable header cell. The sort state lives in a signal, `sortDir`, holding `'ascending'`, `'descending'` or `null` for "not sorted". The first draft of the template is: `<th [colspan]="span()" [aria-sort]="sortDir()" scope="colgroup">Amount</th>` The build fails on one of the two bindings. Understanding why requires knowing how Angular compiles each kind of square-bracket binding. ## Why [colspan] fails A square-bracket binding without a prefix is a **property binding**. Angular resolves it as follows: 1. Is there a component or directive input named `colspan` on this `<th>`? No. 2. Is `colspan` a known DOM property of `th` in Angular's DOM schema? No - the schema lists `colSpan` (and `rowSpan`), and `colspan` is not among the few lowercase names Angular maps to a property (`class`, `for`, `formaction`, `innerHtml`, `readonly`, `tabindex`). 3. So the compiler reports **`NG8002`**: "Can't bind to 'colspan' since it isn't a known property of 'th'." Two correct spellings: | Binding | Writes | Value handling | |---|---|---| | `[colSpan]="span()"` | the `colSpan` DOM property | a number, reflected to the attribute by the browser | | `[attr.colspan]="span()"` | the `colspan` attribute | stringified; removed when `null` or `undefined` | Both render the same table. `[colSpan]` keeps the value typed; `[attr.colspan]` is useful when the value may be `null` and the attribute should disappear. ## Why [aria-sort] compiles Since **Angular v20.2**, a property-style binding whose name starts with `aria-` is compiled to a dedicated **ARIA instruction** rather than an ordinary property write. At runtime it: - sets every directive or component **input** named `aria-sort` on the element, if any exist - and then writes nothing to the DOM; - otherwise writes the **attribute** `aria-sort` with the stringified value; - removes the attribute when the value is `null` or `undefined`. That fits ARIA well. `aria-sort` should only be present on the column that is actually sorted, so returning `null` for the other columns removes it cleanly. The three ARIA spellings now available: - `[aria-sort]="dir"` - the attribute, via the ARIA instruction (the form the Angular accessibility guide now shows); - `[attr.aria-sort]="dir"` - the attribute, via a plain attribute binding (the long-standing idiom, still valid); - `[ariaSort]="dir"` - the DOM's **ARIA reflection property** on `Element`; use property bindings like this mainly for ARIA properties that take structured values, such as `ariaLabelledByElements`, which accepts element references. ## Putting it together A corrected header, sorted state included: 1. `[colSpan]="span()"` for the span. 2. `[aria-sort]="sortDir()"` with `sortDir()` returning `null` for unsorted columns. 3. A `<button>` inside the cell for the click target, so the sort control is keyboard reachable. ## The same pattern elsewhere in tables - `rowspan` behaves exactly like `colspan`: bind `[rowSpan]` or `[attr.rowspan]`, never `[rowspan]`. - `scope` and `headers` exist as properties on table cells, so `[scope]` and `[headers]` compile, but static values are usually enough. - `aria-rowindex`, `aria-colindex`, `aria-rowcount` and `aria-colcount` are useful for virtualised grids and bind the same way as `aria-sort`. - The `span` property belongs to `<col>` and `<colgroup>`, not to header cells. ## Checking your work - Inspect the rendered `<th>`: the sorted column has `aria-sort="ascending"` or `"descending"`, the others have no `aria-sort` attribute at all. - If a table directive on the `<th>` declares an `aria-sort` input, the attribute will not appear, because the input consumed the value; the directive is then responsible for rendering it (for example through a host binding). - Avoid binding `false` or an empty string to mean "not sorted": both produce an attribute value rather than removing it.
- What would [attr.colspan] render when span() returns null, compared with [colSpan]?The attribute binding removes the `colspan` attribute, so the cell falls back to spanning one column. The property binding assigns `null` to the numeric `colSpan` property, which the browser coerces rather than removes. When the absence of a value should mean "no attribute", the `attr.` form states it directly.
- When would you bind [ariaLabelledByElements] instead of an aria- attribute?When the ARIA relationship should point at element references rather than id strings. `ariaLabelledByElements` is a DOM property that accepts an array of elements, so a normal property binding keeps it in sync; the attribute form only accepts space-separated ids.
saying these in an interview costs you the question
- Angular lowercases property names, so [colspan] reaches colSpan
- ARIA attributes can only be bound with the attr. prefix in v22
- Binding false to aria-sort removes the attribute
- [aria-sort] sets the DOM ariaSort property
- [aria-sort] always writes the attribute, even when a directive has that input