An HTML table has two stacked rows of header cells, some spanning several columns. How do you associate each data cell with the right headers when scope is not enough?
answer
- explicit beats inferred association
- each header cell needs an identifier
- cells point at their headers by id
- order the ids so they read as coordinates
- ask first whether the table should be simpler
basics
~20 sGive each header cell an id, then list the relevant ids in each data cell's headers attribute. That explicit association replaces positional guessing and handles multi-row headers and spanning cells that scope alone cannot describe.
solid answer
~50 sUse the `headers`/`id` mechanism. Every header cell gets an `id`, and every `<td>` carries `headers="..."` listing, space-separated, the ids of all header cells that apply to it — typically its column header, the group header spanning above it, and its row label. That is an explicit, unambiguous association, which is exactly what a table with stacked or spanned headers needs, because `scope` can only express "this heads its column" or "this heads its row" and cannot say which of two header rows applies. The cost is real: the markup is verbose, every cell must be maintained, and a wrong id is silent. So the first question to ask is whether the table should be split into two or three simpler tables, each with plain `scope` — a simpler table beats a correctly annotated complex one for every user.
code
html · 21 lines<table>
<caption>Seats by class and deck</caption>
<thead>
<tr>
<td></td>
<th id="lower" colspan="2">Lower deck</th>
</tr>
<tr>
<td></td>
<th id="lower-win">Window</th>
<th id="lower-aisle">Aisle</th>
</tr>
</thead>
<tbody>
<tr>
<th id="economy">Economy</th>
<td headers="economy lower lower-win">88</td>
<td headers="economy lower lower-aisle">72</td>
</tr>
</tbody>
</table>go deeper
Recall that a cell's headers attribute lists the ids of the header cells that apply to it, and that this is for complex tables only. Simple tables use scope.
Explain precisely when scope runs out — stacked header rows, irregular spans — and write a small correct example, including the fact that headers takes ids, not header text.
Weigh the mechanism against its cost: verbose markup, silent typos, uneven assistive-technology support. Show that you would ask whether the table should be split before annotating it, and that you verify associations by listening rather than looking.
Own the policy for data-heavy reporting surfaces: whether complex tables are permitted at all, how association markup is generated from the same source as the data so it cannot drift, and what automated checks gate it.
## Where scope runs out `scope` expresses one relationship: this header cell governs its column, its row, its column group, or its row group. That covers the overwhelming majority of tables. It runs out when the header structure is two-dimensional in a way position cannot resolve — most commonly: - **Stacked header rows.** A top row of group headers ("2024", "2025") each spanning several columns, and a second row of per-column headers ("Q1", "Q2", …) beneath. A data cell needs *both* plus its row label. - **Irregular spans.** `colspan`/`rowspan` cells that make "the header directly above" ambiguous. - **Multiple header cells per row or column** that do not follow a regular grid. In those tables the fallback algorithm and `scope` both under-describe the structure, and users hear the wrong context or none. ## The headers/id mechanism The `headers` attribute goes on a cell and takes a space-separated list of `id` values of header cells **in the same table**. It states, explicitly, which headers apply to that cell: ```html <table> <caption>Revenue by year and quarter</caption> <thead> <tr> <td></td> <th id="y24" colspan="2">2024</th> <th id="y25" colspan="2">2025</th> </tr> <tr> <td></td> <th id="y24q1">Q1</th> <th id="y24q2">Q2</th> <th id="y25q1">Q1</th> <th id="y25q2">Q2</th> </tr> </thead> <tbody> <tr> <th id="emea">EMEA</th> <td headers="emea y24 y24q1">12.4</td> <td headers="emea y24 y24q2">13.1</td> <td headers="emea y25 y25q1">14.0</td> <td headers="emea y25 y25q2">15.2</td> </tr> </tbody> </table> ``` Now the cell reading 15.2 announces as "EMEA, 2025, Q2, 15.2" — the full coordinates, in the order you listed them. That ordering is worth thinking about: put the headers in the order that makes a sentence. Header cells themselves may also carry `headers`, which is how you attach a per-column header to the group header above it when the relationship is not obvious from position. ## Why this is a last resort, not a default The mechanism is correct but expensive: - **Verbosity scales with cells.** A 12 × 20 table means 240 `headers` attributes. Generated markup makes this bearable; hand-written markup does not. - **Errors are silent.** A typo in an id produces no warning, no visual change, and a cell that announces the wrong coordinates. Nothing in a normal test suite catches it. - **It is fragile under edits.** Add a column and every affected cell's attribute must change. - **Assistive-technology support varies.** The mechanism is specified, and major screen readers do use it, but complex-table support has historically been the weakest corner of AT behaviour. A table that requires perfect `headers` handling to be usable is a table balanced on an assumption. And `headers` and `scope` do not mix well on the same cell: state the association one way for the whole table rather than half in each. ## The better first question Before reaching for `headers`, ask whether the table should exist in that shape. Two stacked header rows for years and quarters is usually two tables — one per year — each with a simple header row and `scope="col"`, or one table with an explicit "Year" column. That version is easier for a screen-reader user to navigate, easier on a narrow viewport, easier to generate, and impossible to mis-annotate. Interviewers ask this question partly to see whether you reach for the heavy mechanism or for the simplification. When the complex shape is genuinely required — a published statistical table whose layout is the deliverable — then generate the `headers` attributes programmatically from the same data structure that generates the cells, so the association cannot drift from the content. ## Related pieces `<colgroup>` and `<col>` describe columns structurally and take a `span` attribute, but they are empty elements that do not contain the cells and contribute nothing to header association — do not expect them to substitute for `headers` or `scope`. Likewise `rowspan` and `colspan` create the geometry but assert nothing about meaning. ## How to verify There is no visual check. Read the accessibility tree, or navigate the table with a screen reader's cell-by-cell keys and listen to the announced coordinates at the boundaries of each header group. An automated accessibility checker will catch a `headers` value that references a non-existent id, which is the single most common defect, but it cannot tell you that a cell points at the *wrong* existing header.
- Can you mix headers and scope in the same table?You can, but you should not. They are two ways of stating the same relationship, and mixing them makes the table's association model depend on which mechanism a given assistive technology resolves first for a given cell. Pick one per table: `scope` for regular tables, `headers`/`id` for the irregular ones that need it.
- What would make you refuse to build the complex table at all?If the header structure only exists to compress two datasets into one grid, split it. Two simple tables with one header row each are more navigable cell by cell, survive narrow viewports better, and cannot be mis-annotated. Reserve `headers`/`id` for tables whose two-dimensional shape is genuinely part of the information.
- How would you catch a broken headers attribute in CI?An automated accessibility checker flags `headers` values that reference ids not present in the table, which is the common typo class. It cannot tell you a cell points at the wrong existing header — that needs a rendering test against the generated markup, or generating the attributes from the same data that generates the cells so the two cannot diverge.
saying these in an interview costs you the question
- Thinks headers takes header text rather than ids
- Believes colgroup or col create header associations
- Adds headers to every simple table by default
- Assumes a typo in an id shows up visually
- Says colspan and rowspan are enough to convey the structure