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"
Where a data widget's words come from Layers showing the four sources of what a screen reader says about a data widget: the name, the role, the state and value, and the description. Where a data widget's words come fromNamearia-labelledby, aria-label, caption, label, or content — "Open invoices"Rolefrom the element or role attribute — "grid", "button" — never renamed casuallyState and valuearia-sort, aria-expanded, aria-selected, the cell's textDescriptionaria-describedby — help, units, why a control is unavailable
Name, role and state are read every time; the description is supplementary and may be skipped by users.

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();
A naming pass over one data page Flow of a naming review for a data page: containers, controls, descriptions, role words, and verification in DevTools and element lists. A naming pass over one data pageContainersgrids, tables,charts namedControlsaction + targetDescriptionshelp, reasons,once eachRole wordsstandard, notrenamedVerifytree and elementlists
Work from the outside in — containers, then controls, then the extra information.

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
Naming failures and their fixes Matrix of the most common naming failures in data interfaces with how they sound and how to fix each. Naming failures and their fixesFailureHow it soundsFixUnnamed grid"grid, 40 rows"aria-labelledby → headingGeneric row button"Delete, button" ×40Add the record: "Delete INV-1042"Icon from SVG file"trash-can-outline"aria-hidden icon + text nameHelp in a hover tooltipnothingaria-describedby on the headerRenamed role"Budget, spreadsheet"Domain word in the name
Every row on the left is a real audit finding; every fix on the right is a few characters.

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:

  1. Hidden elements are skipped unless they are referenced directly by aria-labelledby or aria-describedby.
  2. aria-labelledby wins if present and it resolves to text. Each referenced element’s text is computed recursively and the results are joined with spaces.
  3. aria-label is used next, if it is non-empty.
  4. Native labelling comes next: <label for> for form controls, <caption> for tables, <legend> for fieldsets, alt for images, <title> for SVG.
  5. Name from content applies to roles that allow it — buttons, links, cells, headers, options, rows. The text of descendants is concatenated, skipping aria-hidden subtrees.
  6. title is 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.

Distinct entries in the buttons list, 40-row table with 3 row actions Bar chart of how many distinct names a screen reader's buttons list shows for a 40-row table with edit, copy and delete buttons, with generic names versus names that include the record. Distinct entries in the buttons list, 40-row table with 3 row actionsGeneric: Edit, Copy, Delete3 distinct names — 120 buttons, 3 namesAction + record120 distinct names
120 buttons either collapse into 3 indistinguishable names or stay 120 findable ones.

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”.

Permalink to "Related"

← Back to Core ARIA & Keyboard Navigation for Data UIs