skip to content

In an Angular table header written as <th [colspan]="span()" [aria-sort]="sortDir()">, which binding fails to compile, and how should each be written?

level: middleimportance: should knowfreq 40%

answer

  1. one name has no property
  2. the property is camelCase
  3. ARIA names bind directly now
  4. null means no sort attribute
  5. 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 lines
ts
import { 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

for a junior

Remember that [colspan] fails and [colSpan] or [attr.colspan] works, and that ARIA attributes can be bound with [aria-*].

for a middle

Explain NG8002 via the DOM schema, the property-name mapping exceptions, and how the ARIA instruction writes or removes the attribute.

for a senior

Build accessible tables where only the sorted column carries aria-sort, and know when a directive input will consume an aria- binding.

for a principal

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