Accessible Tooltips for Truncated Cell Text
Permalink to "Accessible Tooltips for Truncated Cell Text"Dense grids truncate long values — customer names, descriptions, file paths — with text-overflow: ellipsis. The full text usually appears in a native title tooltip on mouse hover. That leaves out keyboard users (no hover), touch users (no hover), screen magnifier users (tooltip far from the pointer or too small) and, for some screen readers, the tooltip text itself. The good news is that CSS truncation does not remove text from the DOM, so screen readers often read the full value already; the work is in giving sighted keyboard and touch users the same access.
This guide belongs to tooltips, popovers & overlays in data UIs.
Spec reference
Permalink to "Spec reference"- SC 1.4.13 Content on Hover or Focus (AA) — additional content that appears on hover or focus must be dismissible (Escape without moving the pointer or focus), hoverable (the pointer can move onto it without it disappearing) and persistent (stays until dismissed, hover or focus leaves, or it becomes invalid).
- SC 2.1.1 Keyboard (A) — content available on hover must also be available via keyboard.
- SC 1.4.10 Reflow (AA) and SC 1.4.12 Text Spacing (AA) — truncation that hides text under zoom or increased spacing must still allow access to the full text.
- ARIA
tooltiprole — a popup that describes its owner; referenced from the owner witharia-describedby. The nativetitleattribute is not a substitute: it fails 1.4.13’s dismissible and hoverable requirements in most browsers.
When to use — and when not to
Permalink to "When to use — and when not to"Use a truncation tooltip when a column must stay narrow and values occasionally exceed it — names, titles, short descriptions. Show it only when the text is truncated; a tooltip that repeats fully visible text is noise.
Prefer not truncating at all where you can: allow wrapping (with a maximum number of lines) or let users widen the column, as in keyboard accessible column resizing. Tooltips are a fallback, not a layout strategy.
The misapplication: truncating text and moving the full value into aria-label on the cell. That replaces the cell’s content for screen readers, can break aria-describedby relationships, and does nothing for sighted keyboard users.
Annotated code example
Permalink to "Annotated code example"<td class="truncate" tabindex="-1" aria-describedby="tip"> <!-- roving focus in a grid -->
Lund & Co Industrial Supplies — Northern Region Purchasing <!-- full text stays in DOM -->
</td>
<div id="tip" role="tooltip" class="tooltip" hidden></div> <!-- one shared tooltip -->
.truncate { max-inline-size: 18ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tooltip { position: fixed; max-inline-size: 40ch; padding: .4rem .6rem; }
const tip = document.getElementById('tip');
const isTruncated = (el) => el.scrollWidth > el.clientWidth;
function show(cell) {
if (!isTruncated(cell)) { cell.removeAttribute('aria-describedby'); return; } // only when needed
tip.textContent = cell.textContent.trim();
cell.setAttribute('aria-describedby', 'tip');
position(tip, cell);
tip.hidden = false;
}
function hide() { tip.hidden = true; }
grid.addEventListener('focusin', (e) => { const c = e.target.closest('.truncate'); if (c) show(c); }); // keyboard (SC 2.1.1)
grid.addEventListener('focusout', hide);
grid.addEventListener('pointerover', (e) => { const c = e.target.closest('.truncate'); if (c) show(c); });
grid.addEventListener('pointerout', (e) => {
if (!tip.contains(e.relatedTarget)) hide(); // hoverable (SC 1.4.13)
});
tip.addEventListener('pointerleave', hide);
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !tip.hidden) { e.stopPropagation(); hide(); } }); // dismissible
Because the cell already contains the full text, screen readers read it on focus; the aria-describedby duplicates it for some readers. If that duplication is annoying in your testing, skip aria-describedby for truncation tooltips — the tooltip is then a purely visual aid for sighted users, which is acceptable because the information is already programmatic.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected result | AT deviations |
|---|---|---|
| Arrow onto a truncated cell | Full text read; tooltip appears visually | With aria-describedby, some readers repeat the text |
| Escape | Tooltip hides; focus stays on the cell | Escape must not also cancel cell edit or close a dialog |
| Pointer moves onto tooltip | Tooltip stays | — |
| Arrow to a non-truncated cell | No tooltip | — |
| Zoom 200% | More cells truncate; tooltips appear for them | — |
Integration context
Permalink to "Integration context"Tooltips on cells must coexist with the grid’s focus model — roving tabindex or aria-activedescendant. For help text on column headers, the toggletip pattern for column help is usually a better fit than a hover tooltip. The general rules for hover content are in hover cards and WCAG 1.4.13.
Gotchas
Permalink to "Gotchas"Escape conflicts. In a grid, Escape may also cancel edit mode or close a filter popover. When a tooltip is open, Escape should close only the tooltip; stop propagation.
Tooltips covering the next row. A tooltip positioned below the cell can hide the row the user is about to read. Position above or to the side, and never over the focused cell.
Virtualized grids. Recycled cells may change text while a tooltip is open. Hide the tooltip on scroll.
Framework notes
Permalink to "Framework notes"AG Grid has tooltipField and tooltipShowDelay; enable tooltipInteraction: true for hoverable tooltips and verify keyboard focus shows them (recent versions support focus-triggered tooltips). MUI Tooltip shows on focus and hover and closes on Escape; wrap the cell content, not the cell. Angular Material matTooltip appears on focus for focusable hosts; ensure the host is the focused cell.
// MUI: tooltip only when truncated
const [over, setOver] = useState(false);
<Tooltip title={text} disableHoverListener={!over} disableFocusListener={!over}>
<span ref={el => el && setOver(el.scrollWidth > el.clientWidth)} className="truncate" tabIndex={-1}>{text}</span>
</Tooltip>
Worked example
Permalink to "Worked example"A procurement grid truncated supplier names at 20 characters and relied on title. A keyboard-only user could not tell “Lund & Co Industrial…” from “Lund & Co Industrial…” in two adjacent rows — two different suppliers with the same prefix. The fix added focus-triggered tooltips for truncated cells, made the Supplier column resizable with the keyboard, and added a density setting that wraps names onto two lines. Most users chose the wrapping density; the tooltip remained for compact mode.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Make truncation a column option (overflow: 'truncate' | 'wrap' | 'clamp-2') rather than a CSS class teams add ad hoc, and let the grid own the tooltip behaviour. Document that title must not be used for cell content.
FAQ
Permalink to "FAQ"Do screen readers read text hidden by text-overflow?
Yes. CSS truncation only affects painting; the full text remains in the DOM and the accessibility tree.
Is the title attribute ever acceptable?
As a duplicate for mouse users, it does little harm, but it must not be the only way to get the information, and it does not meet 1.4.13.
Should the tooltip show on focus in browse mode?
Screen reader browse mode does not move DOM focus, so tooltips will not appear — which is fine, because the reader already has the full text.
What delay should the tooltip use?
Around 300–500 ms on hover to avoid flicker; on keyboard focus, show it immediately.
Can a truncation tooltip contain links?
No. A tooltip must not contain interactive content. If users need actions, use a popover or toggletip that is opened deliberately.
Related
Permalink to "Related"- Toggletip pattern for column help — help that opens on click
- Hover cards and 1.4.13 — richer hover content
- Auto-fit column width and truncation — avoiding truncation in the first place