User Experience (UX)
Now let's look at how a tooltip behaves for every person, input and setting. A good tooltip is accessible, predictable, designed in every state and adaptive.
Accessible
Pick the right type
Screen readers never see a tooltip open. They read the trigger, so what matters is how the tooltip's text reaches it:
| Type | The tooltip | The trigger | Screen readers hear |
|---|---|---|---|
label | aria-hidden="true", with no role | Keeps its own name, like a visually hidden label | "Bold, button" |
description | role="tooltip", with an id | Points to it with aria-describedby | "Archive, button, Archived projects stay in search" |
Never point to a tooltip with aria-labelledby. It would replace the trigger's own name with the tooltip's text, and an icon-only Button's name comes from its hidden label, as the Button's button/accessible-name rule asks. Hidden text gets translated along with the page, and stays the name when the tooltip's text changes, like a shortcut added to it. With type="label", the Tooltip links nothing at all, and the name stays where it was.
Open it on focus
Keyboard users can't hover, so the tooltip opens the moment its trigger gets focus, without the delay a pointer gets (WCAG 2.1.1). It closes again when focus moves on. That's why the trigger has to be focusable, like a Button or a link. A tooltip on a <span> or an icon on its own never opens for the keyboard, and screen readers have no element to read its description with.
Let people dismiss it
A tooltip covers part of the page, so people need a way to put it away without moving (WCAG 1.4.13). Escape closes it, wherever focus is, and focus stays on the trigger, so keyboard users don't lose their place. A screen magnifier user who's zoomed in can close a tooltip that hides what they were reading, without moving the pointer away from the trigger and losing their spot.
Let the pointer reach it
People need to be able to move the pointer onto a tooltip, whether to read it at a larger zoom or because their hand isn't steady (WCAG 1.4.13). When the pointer leaves the trigger, the tooltip waits 150ms before it closes, which is long enough to cross the offset. Once the pointer is on the tooltip, it stays open until the pointer leaves both.
Contrast
The text needs a contrast of 4.5:1 against the tooltip's fill (WCAG 1.4.3). It's small text, so there's no room to go lower, and with our reference tokens it comes to about 18:1. The fill doesn't need a contrast of its own against the page, since the text is what people read, but a fill that stands out, like ours, shows where the hint starts and ends.
Predictable
Wait before opening on hover
A tooltip that opens the moment the pointer touches its trigger flashes up every time someone crosses the page. On hover, ours waits for the pointer to rest on the trigger for 500ms, which is what we recommend, and opens only if it's still there. Focus doesn't wait, since moving focus is always on purpose.
Once one tooltip has opened, people are usually scanning, like along a toolbar. So moving the pointer straight on to the next trigger opens its tooltip at once, and so does any trigger within 300ms of a tooltip closing. Radix, for example, waits 700ms by default, and skips the delay for 300ms.
Keep it open while it's needed
The tooltip stays open for as long as its trigger has hover or focus, and never closes on a timer (WCAG 1.4.13). Some people read slowly, and some need to zoom in first. It closes when both the pointer and focus have left, or with Escape.
One at a time
Opening a tooltip closes any other one that's open. Two tooltips at once would cover twice as much of the page, and it'd be unclear which one goes with what. That includes a tooltip opened by focus and another by the pointer: the newest one wins.
Keep Escape for the top layer
When a tooltip is open inside something else that closes with Escape, like a Dialog, Escape closes the tooltip and goes no further. Otherwise one key press would close both, and people would lose the dialog along with their work in it.
Our Tooltip listens for Escape only while it's open, in the capture phase on the document, so it comes before a dialog's own handler, and stops it there. It cancels its default action too, so a native <dialog> doesn't close either. With no tooltip open, Escape reaches the dialog as usual. The Popover, Dialog and Dropdown Menu on our Roadmap will follow the same rule, so Escape always closes the layer on top first.
Keep it short and plain
A tooltip holds a few words of plain text, like a name or a short hint. It's never the place for instructions, an error message or anything people need to act on.
Nothing in it can be focused or clicked. A link or a button inside a tooltip can't be reached, since focus would have to leave the trigger, which closes the tooltip. If you need either, use a Popover, which is on our Roadmap.
Don't add a title attribute to the trigger as well. Browsers draw a tooltip of their own for it, which can't be styled, doesn't open for the keyboard and would show up next to yours.
Every state
Closed
The tooltip is in the page, hidden, from the moment it mounts. A description is read with its trigger whether the tooltip is open or not, so screen reader users hear it as soon as they reach the trigger, without anything having to open.
Open
The tooltip is beside its trigger, in the top layer. It stays there while the pointer is on the trigger or on the tooltip, or while the trigger has focus, and it doesn't move once it's open, so people can read it. If the page scrolls, it follows its trigger.
Disabled triggers
A native disabled button leaves the tab order, so keyboard users can never open its tooltip. When a tooltip says why a button is disabled, give the Button focusableWhenDisabled, which sets aria-disabled instead, as the Button's Disabled state recommends. The button stays in the tab order, its tooltip opens on focus and hover, and the Button still blocks the click.
A title is required
<Tooltip label="Add a title to publish">
{(trigger) => (
<Button {...trigger} color="primary" disabled focusableWhenDisabled>
Publish
</Button>
)}
</Tooltip>
<p>A title is required</p>
The reason is on the page too, since a tap never opens a tooltip. A disabled button that only explains itself in a tooltip leaves touch screen users guessing.
Adaptive
Touch screens
Touch screens have no hover, and a tap on a button is meant to press it, so a tap never opens a tooltip. That includes the focus a tap moves to the trigger, which would otherwise open a tooltip that nothing closes until the next tap somewhere else. The reference implementation tells a tap apart by its pointer type, touch, rather than by the screen's size, so a laptop with a touch screen still shows tooltips to its mouse.
That's why a tooltip is never the only place something lives. A label repeats the trigger's own name, and a description adds a hint people can do without, or one that's on the page too, like the reason in the example under Disabled triggers. For a hint people need on every device, use visible text, or a toggletip, a button that shows the hint when it's pressed, which belongs to the Popover on our Roadmap.
More contrast
When someone asks for more contrast (prefers-contrast: more), nothing needs to change. The text already has well over 7:1 against the fill with our reference tokens.
Forced colours
Windows contrast themes replace every colour with the user's own, which drops the tooltip's fill. Ours has a transparent border, which forced colours mode draws in the system's text colour, so its edge stays visible, and it switches to Canvas and CanvasText, the page's own colours, so it reads like the rest of the page. The arrow has the same border on its outer edges, so it stays visible too.
Colour scheme
The fill and its text come from --color--neutral and --color--neutral-contrast, which our reference tokens set with light-dark(). So the tooltip is dark on a light page and light on a dark one, and stands out on both.
Text size and zoom
Sizes are in rem, so the tooltip grows at 200% text size along with everything else (WCAG 1.4.4). Its maximum width is in ch, about 32 characters, so long text wraps onto more lines as it grows rather than running off the screen (WCAG 1.4.10).
Motion
The tooltip fades in and grows from 96% over 0.15 seconds. When someone prefers reduced motion (prefers-reduced-motion: reduce), it only fades (WCAG 2.3.3). It still eases in rather than popping up, since a fade changes nothing's position or size.
Languages and direction
The tooltip's text is yours, so remember to translate it along with the rest of the page. With type="label", translating the trigger's label is enough, since the tooltip shows the same text. start and end follow the reading direction, so a tooltip at the end of a button opens on its right in English and on its left in Arabic, without any change to your code.
UX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
tooltip/focus-opens | Must | Component | Keyboard focus on its trigger opens it at once | WCAG 2.1.1 (A) | Unit test, keyboard |
tooltip/hover-delay | Should | Component | The pointer rests on its trigger for a delay, about 500ms, before it opens | Open Components | Unit test |
tooltip/skip-delay | Should | Component | Moving the pointer straight on to the next trigger opens that tooltip without waiting | Open Components | Unit test |
tooltip/hoverable | Must | Component | The pointer can cross from the trigger onto the tooltip, within a short grace period, without closing it | WCAG 1.4.13 (AA) | Unit test |
tooltip/persistent | Must | Component | It stays open while its trigger has hover or focus, and never hides on a timer | WCAG 1.4.13 (AA) | Unit test |
tooltip/dismiss-with-escape | Must | Component | Escape dismisses it without moving focus or the pointer | WCAG 1.4.13 (AA) | Unit test, keyboard |
tooltip/contain-escape | Must | Component | A key press that closes it stops there, so a dialog or popover around it stays open, while one with no tooltip open reaches them as usual | Open Components | Unit test |
tooltip/one-at-a-time | Must | Component | Opening a tooltip closes any other one that's open | Open Components | Unit test |
tooltip/no-touch | Must | Component | A tap never opens it, even when the tap moves focus to its trigger | Open Components | Unit test |
tooltip/not-the-only-place | Must | Usage | Nothing people need is only in a tooltip, since touch screen users can't open one | Open Components | Review |
tooltip/focusable-trigger | Must | Usage | Its trigger is focusable, and a disabled one uses aria-disabled rather than disabled | Open Components, beyond WCAG 2.1.1 (A) | Keyboard |
tooltip/short | Should | Usage | Its text is a few words long, a hint rather than instructions | Open Components | Review |
tooltip/no-title | Should | Usage | Its trigger has no title attribute, which would show a second, browser-drawn tooltip | Open Components | Review |
FAQ
Tooltip or something else?
A tooltip is one of a few ways to show a hint, and each one suits a different hint:
| When the hint… | Use | Because |
|---|---|---|
| Names an icon-only button | A Tooltip, with type="label" | It shows the name the button already has, on hover and focus |
| Adds a few words people can do without | A Tooltip, with type="description" | Screen readers read it with the trigger, and it stays out of the way |
| Is something people need, like a field's format | Visible text, like an Input's hint | It's there on every device, and while people type |
| Needs a press to show, or holds a link or a button | A Popover, on our Roadmap | It opens on purpose, takes focus and closes when people are done |
title as a tooltip of their own, but only on hover, after a delay and in a style you can't change, and never for the keyboard or on touch screens. It's also why the Tooltip's triggers shouldn't have one, since both tooltips would show up.