Labelling Grids and Tables With aria-labelledby

Permalink to "Labelling Grids and Tables With aria-labelledby"

aria-labelledby names an element using the text of one or more other elements, referenced by id. For data widgets it is usually the best naming mechanism available: the grid’s name is the visible heading above it, so the spoken and visible names cannot drift apart, and a heading edited by a content designer updates the grid’s name automatically. It prevents the two naming failures that dominate audits of data-heavy pages — grids with no name at all (“grid, 40 rows”) and grids whose invisible aria-label says something different from the screen.

This page covers the attribute for role="grid" and treegrid widgets, which have no <caption>, and for tables where a heading already names the data. It belongs to accessible names & descriptions for data widgets.

Spec reference

Permalink to "Spec reference"

aria-labelledby takes a space-separated list of ids (ARIA 1.2). The accessible name computation (accname 1.2) concatenates the text of each referenced element in list order, separated by spaces. It takes precedence over aria-label, <caption>, <label> and native naming. Referenced elements can be hidden — text from a hidden element is still used when referenced directly — which allows off-screen parts of a name.

grid, treegrid, table and listbox all require (or strongly expect) an accessible name. axe-core flags unnamed grids under aria-input-field-name-style rules for some roles and under best-practice rules for others.

Criteria: SC 4.1.2 Name, Role, Value (the widget must have a name), SC 2.4.6 Headings and Labels (the name must describe the content), SC 2.5.3 Label in Name (for widgets with visible labels, the accessible name should contain the visible text).

Accessible name precedence for a grid Layers of the accessible name computation for a grid, from aria-labelledby at the top through aria-label, native caption, and title as the last resort. Accessible name precedence for a gridaria-labelledbytext of the referenced elements, in list orderaria-labela string on the element itself — invisible to sighted userscaption (native table only)the table's own first-child captiontitle attributelast resort; unreliable and shown only on hover
The first layer that produces text wins — which is why aria-labelledby overrides a caption.

When to use aria-labelledby — and when not to

Permalink to "When to use aria-labelledby — and when not to"

Use it for every role="grid" and treegrid, since they have no native naming element. Point it at the visible heading that introduces the grid; if there is none, add one — a visible name helps everyone.

For native <table> elements, prefer <caption> unless a heading directly above the table already says the same thing. Then aria-labelledby pointing at the heading avoids saying it twice.

Do not reference text that does not describe the widget — a nearby instructions paragraph, a “Results” heading shared by three tables. And do not use aria-labelledby to point at a very long element; the whole text becomes the name and is read on every entry.

The misapplication to name is a component library that sets aria-label="Data grid" by default. It overrides nothing if aria-labelledby is also present, but when it is not, every grid in the product is called “Data grid”.

Annotated code example

Permalink to "Annotated code example"
<!-- SC 4.1.2 + 2.4.6: the visible heading is the name -->
<h2 id="inv-heading">Open invoices</h2>
<div role="grid" aria-labelledby="inv-heading">…</div>

<!-- Several ids: name + the current view, both visible on screen -->
<h2 id="tickets-heading">Tickets</h2>
<p id="tickets-view" class="view-summary">Assigned to me · Overdue</p>
<div role="grid" aria-labelledby="tickets-heading tickets-view">…</div>
<!-- name computed as "Tickets Assigned to me · Overdue" -->

<!-- Referencing the grid's own element plus a hidden part is allowed -->
<span id="export-name" hidden>Preview of</span>
<h3 id="export-heading">Export: March invoices</h3>
<table aria-labelledby="export-name export-heading">…</table>
// The view summary is ordinary visible text; updating it updates the grid's name
function setView(filters) {
  document.getElementById('tickets-view').textContent = filters.map((f) => f.label).join(' · ');
  // No announcement needed for the name; the result count is announced separately
}

Combining the heading with a visible view summary gives the grid a name that stays accurate as filters change — the same idea as appending the filter to a table caption in writing table captions and summaries. Because both parts are visible, there is no mismatch for sighted users or for speech-input users who say “click Tickets”.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT-specific deviations
Tab into the grid “Tickets Assigned to me · Overdue, grid, row 2, …” JAWS reads name then role; NVDA role then name
NVDA T (next table) in browse mode “Open invoices, table” for a native table Grids built from divs may not be reached by T
VoiceOver rotor, Tables Name from aria-labelledby —
Heading text edited New name on next entry No announcement — correct
Referenced id missing Falls back to aria-label or nothing axe aria-valid-attr-value flags it
aria-label versus aria-labelledby for a grid Comparison of naming a grid with an aria-label string against pointing aria-labelledby at its visible heading. aria-label versus aria-labelledby for a grid✗ aria-label="Invoices table"Invisible; can differ from the headingNot translated with page content by some toolsDrifts when the heading is editedSpeech users cannot guess it✓ aria-labelledby="inv-heading"The visible heading is the nameTranslated with the pageStays in sync automaticallyMatches what speech users see
One name, visible and spoken, maintained in one place.

Integration context

Permalink to "Integration context"

The name answers “which grid is this”; help about how to read or use it belongs in a description — see aria-describedby for column help text. Controls inside the grid need their own names too, and icon-only row buttons are the usual gap: naming icon-only row action buttons.

For React components, generate the heading id with useId() and pass it to the grid, so reusable components never collide on ids when rendered twice on a page.

Checking a grid's name in DevTools Four steps to verify a grid's accessible name in browser DevTools: select the grid, open the Accessibility pane, read the computed name, and check its source. Checking a grid's name in DevToolsSelect the gridin the Elements panelrole="grid" elementOpen Accessibility paneChrome and Edge; Firefox has an equivalentComputed propertiesRead the name"Tickets Assigned to me · Overdue"Should match the screenCheck the source"From aria-labelledby"Not from aria-label or title
The Accessibility pane shows both the name and which attribute produced it.

Gotchas

Permalink to "Gotchas"

Duplicate ids. Two components rendered on one page with the same hard-coded heading id make both grids take the first heading’s text. Generate ids.

Referencing an element with a lot of text. A heading that contains a badge, a count and a help link produces a long, noisy name. Reference a span containing only the naming text.

Shadow DOM boundaries. aria-labelledby cannot reference an id across a shadow root boundary. Web components that render a grid internally need the heading inside the same root, or an aria-label computed from the heading text.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
Should a data grid use aria-label or aria-labelledby?

aria-labelledby pointing at the visible heading above the grid, whenever there is one. The name then matches what is on screen and stays in sync when the heading changes. Use aria-label only when there is no visible text to reference.

Can aria-labelledby reference more than one element?

Yes. List several ids separated by spaces and the name is their text joined in order. That is useful for combining a heading with a visible description of the current view, such as active filters.

Does aria-labelledby override a table caption?

Yes. aria-labelledby takes precedence over every other naming source, including caption. On a native table with a good caption, you usually do not need it.

Do grid names need to be announced when they change?

No. The new name is read the next time the user enters the grid. Announce the consequence of the change — the new result count — separately.

Permalink to "Related"

← Back to Accessible Names & Descriptions for Data Widgets