Tooltip

User Experience (UX)

How a tooltip behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

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:

TypeThe tooltipThe triggerScreen readers hear
labelaria-hidden="true", with no roleKeeps its own name, like a visually hidden label"Bold, button"
descriptionrole="tooltip", with an idPoints 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.

Add a title to publish

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

RuleLevelScopeRequirementBasisCheck
tooltip/focus-opensMustComponentKeyboard focus on its trigger opens it at onceWCAG 2.1.1 (A)Unit test, keyboard
tooltip/hover-delayShouldComponentThe pointer rests on its trigger for a delay, about 500ms, before it opensOpen ComponentsUnit test
tooltip/skip-delayShouldComponentMoving the pointer straight on to the next trigger opens that tooltip without waitingOpen ComponentsUnit test
tooltip/hoverableMustComponentThe pointer can cross from the trigger onto the tooltip, within a short grace period, without closing itWCAG 1.4.13 (AA)Unit test
tooltip/persistentMustComponentIt stays open while its trigger has hover or focus, and never hides on a timerWCAG 1.4.13 (AA)Unit test
tooltip/dismiss-with-escapeMustComponentEscape dismisses it without moving focus or the pointerWCAG 1.4.13 (AA)Unit test, keyboard
tooltip/contain-escapeMustComponentA 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 usualOpen ComponentsUnit test
tooltip/one-at-a-timeMustComponentOpening a tooltip closes any other one that's openOpen ComponentsUnit test
tooltip/no-touchMustComponentA tap never opens it, even when the tap moves focus to its triggerOpen ComponentsUnit test
tooltip/not-the-only-placeMustUsageNothing people need is only in a tooltip, since touch screen users can't open oneOpen ComponentsReview
tooltip/focusable-triggerMustUsageIts trigger is focusable, and a disabled one uses aria-disabled rather than disabledOpen Components, beyond WCAG 2.1.1 (A)Keyboard
tooltip/shortShouldUsageIts text is a few words long, a hint rather than instructionsOpen ComponentsReview
tooltip/no-titleShouldUsageIts trigger has no title attribute, which would show a second, browser-drawn tooltipOpen ComponentsReview

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…UseBecause
Names an icon-only buttonA Tooltip, with type="label"It shows the name the button already has, on hover and focus
Adds a few words people can do withoutA 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 formatVisible text, like an Input's hintIt's there on every device, and while people type
Needs a press to show, or holds a link or a buttonA Popover, on our RoadmapIt opens on purpose, takes focus and closes when people are done
Copyright © 2026