Accessible Names & Descriptions for Data Widgets
Permalink to "Accessible Names & Descriptions for Data Widgets"Every interactive element and every significant container in a data interface has an accessible name — what a screen reader says to identify it — and may have an accessible description — supplementary information read after it. Names answer “which one is this?”: which grid, which button, which chart. Descriptions answer “what else should I know?”: what a metric means, why a cell is locked, how a table is organised.
Missing and poor names are the most frequent category of finding in audits of data-heavy applications. Unnamed grids (“grid, 40 rows”), forty identical “Delete, button” entries, charts announced as “image”, and icon buttons named after their SVG file are all naming failures, and all are cheap to fix once the rules are clear. This topic covers those rules for the widgets data interfaces are made of. It is written for frontend engineers and for design system maintainers, because most naming bugs originate in reusable components that make the name optional.
It belongs to core ARIA & keyboard navigation for data UIs.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | Relevance to names and descriptions |
|---|---|---|
| 1.1.1 Non-text Content | A | Icons, charts and images have text alternatives that serve the same purpose |
| 1.3.1 Info and Relationships | A | Help text and structure explanations are programmatically associated |
| 2.4.6 Headings and Labels | AA | Names describe the topic or purpose — “Open invoices”, not “Table 1” |
| 2.5.3 Label in Name | A | Accessible names start with, or contain, the visible label text |
| 3.3.2 Labels or Instructions | A | Controls and inputs have labels; unusual columns have explanations |
| 4.1.2 Name, Role, Value | A | Every interactive element and widget exposes a name and the correct role |
Prerequisites
Permalink to "Prerequisites"- The role of each component is settled first — a name on the wrong role does not help. See choosing between grid and table roles.
- Native tables use
<caption>and header cells, covered in writing table captions and summaries. - Familiarity with the accessibility tree in browser DevTools, where computed names and their sources are shown.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"| Mechanism | Valid values | When to apply | Common misuse |
|---|---|---|---|
aria-labelledby |
id list | Name from visible text: grid from heading, button from row header | Referencing long or unrelated text; duplicate ids |
aria-label |
string | Name when no visible text exists | Library defaults like “Data grid”; drift from visible labels |
<caption> |
text | Native tables | Missing; “Table 1”; not first child |
| visually hidden text | text in the element | Icon buttons, abbreviations | display: none instead of a clip-based class |
aria-describedby |
id list | Help text, errors, reasons for disabled state | On every cell of a column |
aria-roledescription |
localised string | Rare container cases such as slides | Renaming grids, cells, buttons |
title |
string | Supplementary hover text only | As the only name for a control |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Name every data container (SC 4.1.2, 2.4.6)
Permalink to "Step 1 — Name every data container (SC 4.1.2, 2.4.6)"<h2 id="inv-h">Open invoices</h2>
<div role="grid" aria-labelledby="inv-h">…</div>
Step 2 — Name every control with action and target (SC 4.1.2, 2.5.3)
Permalink to "Step 2 — Name every control with action and target (SC 4.1.2, 2.5.3)"<button type="button" class="icon-btn" data-tooltip="Delete">
<span class="icon icon-bin" aria-hidden="true"></span>
<span class="visually-hidden">Delete INV-1042</span>
</button>
Step 3 — Attach descriptions once per concept (SC 1.3.1, 3.3.2)
Permalink to "Step 3 — Attach descriptions once per concept (SC 1.3.1, 3.3.2)"<button class="sort" aria-describedby="help-nrr">NRR</button>
<p id="help-nrr" hidden>Net revenue retention: …</p>
Step 4 — Keep role words standard (SC 4.1.2)
Permalink to "Step 4 — Keep role words standard (SC 4.1.2)"<div role="grid" aria-label="Budget spreadsheet">…</div> <!-- not aria-roledescription -->
Step 5 — Verify in the tree and in element lists
Permalink to "Step 5 — Verify in the tree and in element lists"// Playwright: every grid has a name
for (const g of await page.getByRole('grid').all())
expect(await g.getAttribute('aria-labelledby') || await g.getAttribute('aria-label')).toBeTruthy();
Keyboard interaction contract
Permalink to "Keyboard interaction contract"Names and descriptions do not add keys; they decide what is heard when existing keys move focus.
| Key | Context | Action | Expected AT announcement | Failure indicator |
|---|---|---|---|---|
Tab |
Into a grid | Focus first cell | “Open invoices, grid, …” | “grid” with no name |
Tab |
Row icon button | Focus button | “Delete INV-1042, button” | “button” or “Delete, button” everywhere |
Tab |
Sort button with help | Focus button | “NRR, button” … then description | Help unavailable or read on every cell |
Insert+F7 (NVDA) |
Anywhere | Elements list | Unique, meaningful entries | Duplicates and file names |
| Voice: “click Delete” | Anywhere | Speech input | Matching buttons highlighted | No match — label not in name |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"| AT + browser | aria-labelledby names | aria-describedby | aria-roledescription |
|---|---|---|---|
| NVDA + Chrome | Read on focus | Read after a pause; user can disable | Replaces role word |
| NVDA + Firefox | Read on focus | Read after a pause | Replaces role word |
| JAWS + Chrome | Read on focus | Read; depends on verbosity | Replaces role word |
| VoiceOver + Safari | Read on focus | Read after a delay; hint setting | Replaces role word, including rotor |
| TalkBack + Chrome | Read on focus | Read after name and role | Replaces role word |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. The component library default name
Permalink to "1. The component library default name"Diagnosis: every grid is “Data grid” because the library sets aria-label when none is passed. Fix: remove the default and require a label prop or a labelledby id.
2. Names that include live values
Permalink to "2. Names that include live values"Diagnosis: a KPI card named “Revenue 1.2M up 4%” — the name changes on every refresh and is announced as new content by some readers. Fix: name it “Revenue”; put the value in the content.
3. Descriptions that repeat the name
Permalink to "3. Descriptions that repeat the name"Diagnosis: aria-describedby points at the same heading as aria-labelledby, so the name is read twice. Fix: descriptions carry only extra information.
4. Hidden text removed by display: none
Permalink to "4. Hidden text removed by display: none" Diagnosis: a “visually hidden” class implemented with display: none removes the text from the accessibility tree, leaving the button unnamed. Fix: use a clip-based class.
5. Label in name mismatch
Permalink to "5. Label in name mismatch"Diagnosis: a button showing “Export” has aria-label="Download CSV file"; voice users saying “click Export” get no match. Fix: start the name with the visible text: “Export as CSV”.
How the name is computed, in brief
Permalink to "How the name is computed, in brief"The accessible name computation (accname) runs the same steps for every element, and knowing its order explains most surprises:
- Hidden elements are skipped unless they are referenced directly by
aria-labelledbyoraria-describedby. aria-labelledbywins if present and it resolves to text. Each referenced element’s text is computed recursively and the results are joined with spaces.aria-labelis used next, if it is non-empty.- Native labelling comes next:
<label for>for form controls,<caption>for tables,<legend>for fieldsets,altfor images,<title>for SVG. - Name from content applies to roles that allow it — buttons, links, cells, headers, options, rows. The text of descendants is concatenated, skipping
aria-hiddensubtrees. titleis the last resort.
Two consequences matter for data widgets. First, roles that do not take a name from content — grid, table, listbox, region, dialog — get no name at all unless one of steps 2–4 supplies it; that is why unnamed grids are so common. Second, a button’s name from content includes every descendant that is not hidden, so an icon with a stray <title> or an icon font’s glyph ends up in the name unless it is marked aria-hidden.
Descriptions follow a shorter path: aria-describedby first, then aria-description where supported, then title if it was not already used for the name. A title on a named button therefore becomes its description, which is one reason tooltips implemented with title are read unexpectedly by some screen readers.
Labelling grids with aria-labelledby
Permalink to "Labelling grids with aria-labelledby"Labelling grids and tables with aria-labelledby covers naming grids from visible headings, combining several ids to include the current view, and checking the computed name and its source in DevTools.
<div role="grid" aria-labelledby="tickets-heading tickets-view">…</div>
Behaviour note: the name is read on entry and never announced when it changes — which is correct.
Column help text with aria-describedby
Permalink to "Column help text with aria-describedby"aria-describedby for column help text attaches metric definitions to column headers, pairs them with a keyboard-operable info panel for SC 1.4.13, and explains why attaching them to cells makes grids unusable.
<button class="sort" aria-describedby="help-nrr">NRR</button>
Behaviour note: descriptions are read after a pause and can be disabled by users, so nothing essential should live only there.
Naming icon-only row action buttons
Permalink to "Naming icon-only row action buttons"Naming icon-only row action buttons gives each pencil, copy and bin icon a name with the action and the record, three ways, and ties the name to the visible tooltip for speech-input users.
<span className="visually-hidden">{`${action} ${row.id}`}</span>
Behaviour note: check the screen reader’s buttons list — duplicates there are duplicates for users.
When to use aria-roledescription
Permalink to "When to use aria-roledescription"When to use aria-roledescription sets out the narrow container cases where renaming a role helps and the widget cases — grids, cells, buttons — where it removes the cue users rely on to know which keys work.
<div role="grid" aria-label="Budget spreadsheet">…</div>
Behaviour note: role words are part of the keyboard contract; names are where domain vocabulary belongs.
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Names and localisation. Names built from visible text are translated with the page. Names in aria-label strings live in code and are often missed by translation pipelines. Prefer visible text and hidden spans in localised products, and include aria-label strings in the extraction if you use them.
Names and live updates. Names should be stable. Values change; names identify. When a name must reflect a view — “Tickets, assigned to me” — make the changing part visible text that users can see, and do not announce the name change itself.
Descriptions and verbosity. Many experienced screen reader users turn descriptions down or off. Treat descriptions as helpful extras, never as the only place a requirement or an error is stated. Errors belong in the field’s description and in visible text next to the field.
Names in virtualised content. Rows that scroll out of the DOM take their ids with them; an aria-labelledby pointing at a row header that is not rendered produces an empty name. Build names for controls in virtualised rows from data, with hidden text, rather than from references.
Design system integration
Permalink to "Design system integration"| Component | Naming requirement the component should enforce | Criterion |
|---|---|---|
| DataTable / DataGrid | Required label or labelledBy prop; no default name |
4.1.2, 2.4.6 |
| IconButton | Required label; icon rendered aria-hidden; tooltip derived from the label |
4.1.2, 2.5.3, 1.1.1 |
| RowActions | Requires a rowLabel; generates “Action rowLabel” names |
4.1.2 |
| ColumnHeader | Optional help prop wired to aria-describedby and an info disclosure |
1.3.1, 1.4.13 |
| Chart | Required title and summary; description linked to the data table | 1.1.1 |
Required props are the single most effective naming control a design system has: the build fails, or the component throws, when a name is missing, so the fix happens before review rather than after an audit.
Testing checklist
Permalink to "Testing checklist"Automated
Permalink to "Automated"Keyboard and speech input
Permalink to "Keyboard and speech input"Screen reader
Permalink to "Screen reader"FAQ
Permalink to "FAQ"What is the difference between an accessible name and an accessible description?
The name identifies an element and is read every time it gets focus — “Delete INV-1042”. The description adds supplementary information, is read after the name and role, and can be turned down by users. Essential information belongs in the name or visible text.
What is the best way to name a data grid?
aria-labelledby pointing at the visible heading above it, so the spoken and visible names match. For native tables, a caption does the same job.
Should a chart have a name, a description, or both?
Both. The name identifies the chart in a sentence — “Revenue by region, 2026” — and the description summarises what it shows, such as the trend or the largest value. Link to a data table for the full detail rather than packing the numbers into the description.
Can an accessible name be too long?
Yes. Names are read on every focus and appear in element lists, so they should identify, not explain. A dozen words is plenty for most controls and containers; move anything longer into a description or visible help.
How do I check an element's computed name?
Open the browser DevTools Accessibility pane with the element selected. It shows the computed name, the computed description and which attribute or element each came from, which is faster and more reliable than listening for it.
Why do audits flag so many buttons in data tables?
Because row action buttons are usually icon-only and either unnamed or named identically on every row. Each needs a name with the action and the record, such as “Edit INV-1042”.
Related
Permalink to "Related"- Composite widget roles & states — roles, the third part of name-role-value
- Semantic HTML table construction — captions and headers
- Row actions & context menus — naming menus and their items
- Chart alternatives — naming and describing charts
- Automated testing pipelines — rules that catch missing names