Toggletip Pattern for Column Help
Permalink to "Toggletip Pattern for Column Help"Data columns often need explanation: what “ARR” means, how “Risk score” is calculated, whether “Amount” includes tax. The common solution is a hover tooltip on the header — invisible to keyboard and touch users, and often triggered accidentally by mouse users heading for the sort control. A toggletip is a better fit: a small “i” button beside the header that shows help when the user asks for it, and keeps it visible until they are done.
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) — does not apply to content shown on click, which is one reason toggletips sidestep many tooltip problems; the popup should still be dismissible.
- SC 2.1.1 Keyboard (A) — the button and its popup are keyboard operable.
- SC 4.1.2 Name, Role, Value (A) — the button’s name identifies the column (“About Risk score”);
aria-expandedreflects state if the popup is a disclosure. - SC 4.1.3 Status Messages (AA) — with the live-region variant, the help is announced when shown.
- SC 2.5.8 Target Size (Minimum) (AA) — small “i” icons need a 24×24 pixel target or spacing from the sort button.
When to use — and when not to
Permalink to "When to use — and when not to"Use a toggletip when header help is longer than a few words, when it is needed occasionally rather than constantly, or when the header already contains a sort button (so a hover tooltip would collide with it).
Use aria-describedby with visible or hidden text when the help is a short clarification that every screen reader user benefits from hearing on focus (“Amount, including VAT”). Use a dialog or a help panel when the explanation includes tables, formulas or multiple paragraphs.
The misapplication: putting the help in a tooltip on the sort button. Users then hear (or see) help every time they try to sort, and the help disappears exactly when they move to read it.
Annotated code example
Permalink to "Annotated code example"<th scope="col" aria-sort="none">
<button type="button" class="sort">Risk score</button>
<!-- Separate help button, named with the column (SC 4.1.2) -->
<span class="toggletip">
<button type="button" class="info" aria-label="About Risk score" aria-expanded="false">i</button>
<span role="status" class="toggletip-bubble"></span> <!-- live region, present on load -->
</span>
</th>
document.querySelectorAll('.toggletip').forEach((tt) => {
const btn = tt.querySelector('.info');
const bubble = tt.querySelector('[role="status"]');
const help = HELP[btn.getAttribute('aria-label')]; // e.g. text from column definition
const open = () => {
bubble.textContent = '';
requestAnimationFrame(() => { bubble.textContent = help; }); // announced via the status region (SC 4.1.3)
btn.setAttribute('aria-expanded', 'true');
};
const close = () => { bubble.textContent = ''; btn.setAttribute('aria-expanded', 'false'); };
btn.addEventListener('click', () => (btn.getAttribute('aria-expanded') === 'true' ? close() : open()));
btn.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });
document.addEventListener('click', (e) => { if (!tt.contains(e.target)) close(); });
});
.info { inline-size: 24px; block-size: 24px; margin-inline-start: 4px; } /* SC 2.5.8 */
.toggletip-bubble:empty { display: none; }
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | AT deviations |
|---|---|---|
| Tab to info button | “About Risk score, button, collapsed” | — |
| Enter | “Risk score: 0 to 100, based on payment history and open disputes.” | VoiceOver may read “expanded” first |
| Enter again | Bubble closes | — |
| Escape | Bubble closes; focus stays | Escape must not also close the containing dialog |
| Tab away | Bubble stays until outside click or Escape (or closes — choose and document) | — |
Integration context
Permalink to "Integration context"Header help complements aria-describedby for column help text, which covers short descriptions heard on focus. Sort buttons in the same header follow aria-sort attributes for accessible column filtering. If help needs links, use a non-modal popover as in the Popover API for data grid overlays.
Gotchas
Permalink to "Gotchas"Toggletips inside role=“grid” headers. In an interactive grid, header cells are part of the arrow-key model. Make the info button reachable with Tab from the header cell, or with Enter that moves into the cell’s controls, consistent with how the sort button is reached.
Duplicate announcements. If the bubble is a status region and you also move focus into it, the text is read twice. Choose one.
Translated help. Long help text in some languages overflows narrow bubbles; set a max width and allow wrapping, never truncation.
Framework notes
Permalink to "Framework notes"Many component libraries call this pattern a “popover” or “info tip”. Radix Popover opens on click with focus moved into content — suitable when the help contains links. React Aria has a DialogTrigger with Popover for the same. A pure toggletip with a live region is small enough to build directly, as above; Angular projects often use the CDK Overlay for positioning and the CDK LiveAnnouncer for the text.
// Angular: announce help via LiveAnnouncer instead of an inline status region
open() { this.expanded = true; this.announcer.announce(this.helpText, 'polite'); }
Worked example
Permalink to "Worked example"A finance grid has columns “ARR”, “NRR” and “Logo churn”. New staff hovered headers to see definitions — when they remembered — and screen reader users had no access to them at all. Each header now has an “About ARR” button that opens a short definition, read aloud via the status region. The definitions also feed a “Column definitions” help dialog linked from the table toolbar, which includes formulas. Support questions about column meanings fell noticeably, and the definitions finally matched across products because they came from one source.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Store column help in the column definition (help: { short, long }) and render the toggletip, the aria-describedby short form and the definitions dialog from it. One source of truth keeps meanings consistent and translatable.
FAQ
Permalink to "FAQ"What is the difference between a tooltip and a toggletip?
A tooltip appears on hover or focus and describes its trigger; a toggletip appears on click and is its own control. Toggletips suit help that users seek out deliberately.
Should the toggletip move focus?
Not for plain text — announce it through a live region and keep focus on the button. If the content has links, move focus into it like a popover.
Is "i" a good visible label?
It is widely recognised, but the accessible name must be specific (“About Risk score”). Voice control users may say “click About Risk score”, so consider a visible tooltip with that text on hover.
Can I put help in the column header text?
For very short qualifiers, yes: “Amount (incl. VAT)”. It is the most accessible option of all.
Should help close when the user tabs away?
Either is acceptable; closing on blur is common for plain-text toggletips. Document the behaviour and be consistent.
Related
Permalink to "Related"- aria-describedby for column help text — short help on focus
- Truncated cell tooltips — revealing full values
- Column header menus — another header control