aria-describedby for Column Help Text

Permalink to "aria-describedby for Column Help Text"

Data grids are full of columns whose meaning is not obvious from a two-word header: “ARR”, “Net retention”, “P95 latency”, “Adj. margin”. Sighted users get an info icon with a tooltip. Screen reader and keyboard users often get nothing, because the tooltip opens on hover and its text is not connected to the header. aria-describedby connects it: the header keeps its short name, and the help text is available as a description whenever focus reaches the header.

This page covers where to attach column help, how to make the same text available to sighted keyboard users, and the one placement that makes grids worse. It belongs to accessible names & descriptions for data widgets.

Spec reference

Permalink to "Spec reference"

aria-describedby references one or more elements by id; their text becomes the element’s accessible description, computed separately from the name. Screen readers read the description after the name and role, usually after a short pause, and many let users suppress descriptions in verbosity settings. Hidden referenced elements (hidden, display: none) still contribute text when referenced directly.

aria-description (ARIA 1.3 draft) takes a string instead of an id reference. Support is growing but not universal; aria-describedby remains the dependable choice.

Criteria: SC 3.3.2 Labels or Instructions and SC 1.3.1 Info and Relationships (the help is programmatically associated), SC 1.4.13 Content on Hover or Focus (the visible tooltip must be dismissible, hoverable and persistent).

Where column help should be attached Comparison of attaching column help text to the column header's control against attaching it to every data cell in the column. Where column help should be attached✓ On the header or its sort buttonRead when the header gets focusHeard once per columnAvailable on demand in browse modeCell reading stays short✗ On every data cellRead after every value in the column"4.2%, Net revenue retention compares…"Arrowing down the column becomes slowUsers disable descriptions entirely
Attach once, where users look for help — not to the hundreds of cells they read quickly.

When to use column descriptions — and when not to

Permalink to "When to use column descriptions — and when not to"

Use them for columns whose meaning, unit or computation a reasonable user might not know: financial ratios, derived metrics, abbreviations, statuses with specific definitions. Keep each description to one or two sentences.

Do not use them for information that belongs in the header itself. If every user needs to know the unit, put it in the header text: “Revenue (EUR)”. A description is for detail some users need sometimes.

The misapplication to name is aria-describedby on each <td> or gridcell pointing at the column’s help text. It works — and turns a quick scan down a column into a paragraph per cell.

Annotated code example

Permalink to "Annotated code example"
<!-- Help text: one element per column, reused by header and tooltip -->
<p id="help-nrr" class="col-help" hidden>
  Net revenue retention: this year's revenue from last year's customers,
  as a percentage of last year's revenue from them. Above 100% means growth.
</p>

<table>
  <caption>Accounts by retention</caption>
  <thead>
    <tr>
      <th scope="col">Account</th>
      <th scope="col">
        <!-- SC 1.3.1 + 3.3.2: the sort control carries the description -->
        <button type="button" class="sort" aria-describedby="help-nrr">NRR</button>
        <!-- SC 1.4.13: the same text, on demand, for sighted keyboard users -->
        <button type="button" class="info" aria-expanded="false"
                aria-controls="tip-nrr" aria-label="About NRR">ⓘ</button>
        <div id="tip-nrr" role="tooltip" class="tooltip" hidden></div>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">Northwind</th><td>112%</td></tr>   <!-- no description on cells -->
  </tbody>
</table>
// Info button toggles a persistent, dismissible panel with the same text (SC 1.4.13)
document.querySelectorAll('th .info').forEach((btn) => {
  const tip = document.getElementById(btn.getAttribute('aria-controls'));
  const src = document.getElementById(btn.previousElementSibling.getAttribute('aria-describedby'));
  tip.textContent = src.textContent.trim();
  btn.addEventListener('click', () => {
    const open = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!open));
    tip.hidden = open;
  });
  btn.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') { btn.setAttribute('aria-expanded', 'false'); tip.hidden = true; }
  });
});

The help element is hidden and still works as a description source, because aria-describedby reads hidden elements referenced directly. The visible disclosure uses a copy of the same text, so there is one source to edit.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT-specific deviations
Tab to the NRR sort button “NRR, button” … “Net revenue retention: this year’s…” NVDA reads the description after a pause; can be turned off
Browse mode onto the header cell “NRR, column header” Descriptions on the inner button are not read in browse mode by all readers
Arrow down the column “112%” per cell Unchanged — no descriptions on cells
Enter on the info button “About NRR, button, expanded” Panel text reachable by reading on
Escape on info button Panel closes Required by SC 1.4.13
Getting help for one column Timeline of a keyboard user reaching a column header, hearing its description, opening the info panel and returning to reading the column. Getting help for one columnTab to NRRname and role readPausedescription readInfo buttonpanel opens with the same textEscapepanel closesRead columncells read without help textone column, one explanation
The description is there when focus reaches the header, and silent for every cell below it.

Integration context

Permalink to "Integration context"

Grid-level help — how to navigate, which keys edit — belongs in a description on the grid itself or in a keyboard help dialog, not in column descriptions; see keyboard shortcut help dialogs. Column descriptions also explain why a whole column is locked, which saves describing each disabled cell as in aria-readonly and aria-disabled in grid cells.

If units are the only thing users need, they belong in the header text, as covered in formatting numbers and units in table cells.

Words heard reading ten cells down a column Bar chart comparing words heard when reading ten cells down a column with the help text attached to the header versus attached to each cell. Words heard reading ten cells down a columnHelp on the header45 wordsHelp on every cell270 words — scanning becomes listening
The same 25-word help text, attached once or ten times.

Gotchas

Permalink to "Gotchas"

Hover-only tooltips. A tooltip that appears only on mouse hover fails keyboard users and SC 1.4.13. Use a button that toggles persistent content.

title attributes as help. title on a header is read inconsistently and cannot be reached by keyboard. Replace it with a described-by element.

Very long descriptions. A 100-word methodology note is documentation, not a description. Link to it from the panel instead.

Design system notes

Permalink to "Design system notes"

A column definition in a table component is the natural home for help text: { key: 'nrr', header: 'NRR', help: 'Net revenue retention: …' }. From that one field the component can render the hidden description element, wire aria-describedby on the header’s sort button, and render the info disclosure — so every product that uses the column gets the same accessible help, and no product can attach it to cells by accident. Keep the help text in the same content system as other UI copy, so it is reviewed and translated with everything else.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
How do I add help text to a data table column for screen reader users?

Write the help text in an element with an id and reference it with aria-describedby from the column header’s sort button or header cell. Screen readers read it as a description when focus reaches the header.

Should each cell in the column reference the help text?

No. That repeats the help after every value and makes scanning a column slow. Attach it once, to the header.

Can aria-describedby reference a hidden element?

Yes. Text from a hidden element is still used when the element is referenced directly by aria-describedby, which lets you keep the help out of the visual layout while exposing it to assistive technology.

Is a tooltip enough for column help?

Only if it is keyboard operable, dismissible with Escape, and stays visible while hovered or focused, as SC 1.4.13 requires. A button that toggles a small panel is the most reliable form.

Permalink to "Related"

← Back to Accessible Names & Descriptions for Data Widgets