Tooltip

A short label that appears on hover or focus, for naming a control that shows no text of its own.

Accessibility

What Tooltip does for you, and what it leaves to the page around it.

How the bubble is wired to the trigger

Tooltip adds no element of its own. It adds one attribute to your child, and the bubble itself renders into document.body. Both matter for what a screen reader gets.
  • The popup container carries role="tooltip" and a generated id.
  • The trigger is joined to it with aria-describedby, not aria-labelledby. An existing aria-describedby on the child is preserved and the tooltip's id is appended to it, so a field already pointing at its own help text keeps that pointer.
  • The attribute is only there while the bubble is open. A closed tooltip adds nothing to the trigger's accessible description.
  • The attribute goes on your child, so the child has to be a real DOM element or a component that spreads its props onto one. A custom component that drops {...props} leaves the bubble connected to nothing.
  • An empty or null title keeps the tooltip closed, so an empty bubble is never announced as an empty description.

Hover is not enough, so focus has to work

trigger defaults to ["hover", "focus"]. The focus half is the only way a keyboard reader sees the hint, and it is easy to switch off by accident.
  • Writing trigger="hover" removes focus. The prop replaces the default array instead of adding to it. If you need click, pass trigger={["click", "focus"]} rather than "click" alone.
  • The trigger has to be focusable in the first place. Focus is an event on the child; a <span> or a <div> never receives one. Put the tooltip on a <button> or a link, or give the element a real role and tabIndex={0}.
  • A plain text child is wrapped in a <span>, which is not focusable. A tooltip on a word of text only reaches a pointer.
  • A hint on a non-interactive glyph (a status dot, an info icon) is unreachable by keyboard for the same reason. If the glyph carries meaning, put that meaning in text the reader can reach, and treat the tooltip as the shortcut rather than the source.

Never a control's only name

aria-describedby supplies a description, and only while the bubble is open. An icon-only button whose meaning lives entirely in its tooltip is an unnamed button for every reader who has not opened it, which includes anyone browsing a list of controls.
  • Give the control its own aria-label, or an sr-only word inside it, or point aria-labelledby at text that already says it. The tooltip then repeats that name for sighted pointer users.
  • Keep the two saying the same thing. A button labeled “Delete” whose bubble says “Remove from list” gives two readers two different products.
  • Where the bubble carries more than the name (a shortcut or a caveat), the extra is the description and belongs in the tooltip. The name stays on the control.
  • Do not put a focusable element inside title on a hover-triggered tooltip. It is inside a role="tooltip" region that closes when the pointer leaves, so a tab stop in there is a tab stop into something that is about to disappear. Use Popover with a click trigger.

A disabled trigger

A tooltip on a disabled control is one of the best uses of the component, because a disabled button with no explanation is a dead end. Which trigger you pick decides whether it opens at all.
  • Hover still works on a bare disabled <button>. It still fires the hover events, so the default tooltip opens on it with no wrapping.
  • A click trigger never opens on it. A disabled button fires no click, so a trigger="click" tooltip on it never opens.
  • Wrap the button in a <span> and put pointer-events-none on the button, so the click lands on the span instead.
  • Focus does not reach a disabled button either. It is out of the tab order, so the explanation is unreachable by keyboard whichever trigger you pick. Where the reason matters, say it on the page, or use aria-disabled on a control that stays focusable and refuses the action itself.

Touch

There is no hover on a touch screen. A tap resolves to the control's own action, so a hover-triggered tooltip on a phone either never opens or opens at the same moment the button fires.
  • Treat every tooltip as invisible on touch, and check that the screen still makes sense with all of them removed. If it does not, the content was never tooltip content.
  • Where a hint has to be reachable by tap, give it its own affordance: an info button with trigger={["click", "focus"]}, or Popover. A tap target that opens an explanation is a control, and it should look like one.
  • A tooltip on a disabled control is worst here: no hover, no focus, and no click. Put the reason in visible text on small screens.
  • Increasing mouseLeaveDelay does not help touch. It is a pointer timer, and there is no pointer.

Contrast and motion

  • The default bubble is colorBgSpotlight with colorTextLightSolid text. A preset color changes the background and keeps the white text, so bright presets like yellow, gold and lime fall well under 4.5:1. A custom CSS color gets black or white text picked from its brightness, so check that pair too.
  • Anything you paint inside the bubble is on that surface, not the page canvas. A text-secondary from the page palette can vanish on it. Dim the inherited light-solid ink with opacity-70 instead, which stays legible whatever color the bubble carries.
  • The default type step is 13px, and size="small" takes it to 12px. Reach for small in dense toolbars, not as a default: small type on a transient surface is the hardest reading on the page.
  • The arrowless bubble's thin border only shows in light mode. Without it, a light bubble with no arrow has nothing separating it from a light page.
  • transition={false} removes the open and close animation, which suits a reader who has asked for reduced motion. The motion prop can change the animation but not remove it. To turn motion off for the whole theme, set token: { motion: false }.
  • Do not animate the content of a bubble that is already zooming in. The movement is decoration on something that appears and disappears by itself, and it competes with the reading.