A short visual label beside a control, such as "Zoom in" over the map's zoom button, shown while a mouse pointer rests on the control or the keyboard focuses it, for sighted mouse and keyboard users.
When to use
Use Tooltip for a short visual label beside a control, such as "Zoom in" over an icon-only button, for sighted mouse and keyboard users.
| Use instead | When |
|---|---|
| MapTooltip | The card sits against a point on the map, such as the vehicle under the pointer |
| Popover | The reader needs a sentence, a link or a control, which a tooltip can't hold |
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The tooltip's words: a short label in sentence case that matches the trigger's accessible name, such as "Zoom in". It takes the popup's inverse text colour. |
trigger* | ReactElement<unknown, JSXElementConstructor<any> | string> | — | Required. The control the tooltip labels, such as an IconButton, rendered in place of the tooltip. It must take a ref and spread the props it is given, as Perron's buttons do, and carry its own accessible name. |
open | boolean | — | Whether the tooltip is shown. Set it to control the tooltip, with onOpen and onClose to hear when it asks to change; leave it out for a tooltip its trigger opens. |
defaultOpen | boolean | false | Whether the tooltip is shown at first, when open is left out. |
onOpen | (event: Event, details: Tooltip.Root.ChangeEventDetails) => void | — | Called when the tooltip asks to open: reason in details is 'trigger-hover' when the pointer rests on the trigger or 'trigger-focus' when the keyboard focuses it, and details.cancel() keeps it closed. |
onClose | (event: Event, reason: Tooltip.Root.ChangeEventReason, details: Tooltip.Root.ChangeEventDetails) => void | — | Called when the tooltip asks to close: reason is 'trigger-hover', 'trigger-focus', 'trigger-press', 'escape-key', 'outside-press' or another of Base UI's reasons, and details.cancel() keeps it open. |
ref | Ref<HTMLDivElement> | — | Reaches the root, the popup. |
slotProps | { trigger?: Props<unknown> | undefined; portal?: TooltipPortalProps | undefined; positioner?: TooltipPositionerProps | undefined; } | — | Props for each part: trigger, portal, positioner. |
childrenReactNode, default —- The tooltip's words: a short label in sentence case that matches the trigger's accessible name, such as "Zoom in". It takes the popup's inverse text colour.
trigger (required)ReactElement<unknown, JSXElementConstructor<any> | string>, default —- Required. The control the tooltip labels, such as an IconButton, rendered in place of the tooltip. It must take a ref and spread the props it is given, as Perron's buttons do, and carry its own accessible name.
openboolean, default —- Whether the tooltip is shown. Set it to control the tooltip, with
onOpenandonCloseto hear when it asks to change; leave it out for a tooltip its trigger opens. defaultOpenboolean, defaultfalse- Whether the tooltip is shown at first, when
openis left out. onOpen(event: Event, details: Tooltip.Root.ChangeEventDetails) => void, default —- Called when the tooltip asks to open:
reasonindetailsis'trigger-hover'when the pointer rests on the trigger or'trigger-focus'when the keyboard focuses it, anddetails.cancel()keeps it closed. onClose(event: Event, reason: Tooltip.Root.ChangeEventReason, details: Tooltip.Root.ChangeEventDetails) => void, default —- Called when the tooltip asks to close:
reasonis'trigger-hover','trigger-focus','trigger-press','escape-key','outside-press'or another of Base UI's reasons, anddetails.cancel()keeps it open. refRef<HTMLDivElement>, default —- Reaches the root, the popup.
slotProps{ trigger?: Props<unknown> | undefined; portal?: TooltipPortalProps | undefined; positioner?: TooltipPositionerProps | undefined; }, default —- Props for each part:
trigger,portal,positioner.
Use it
import { Tooltip } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Tooltip" })Keyboard and ARIA
From the Base UI reference for Tooltip and the installed TooltipRoot, TooltipTrigger, TooltipPopup and the focus, hover and dismiss hooks they build on. "From" says who does it.
| Key | Does | From |
|---|---|---|
Tab, Shift+Tab | Moving focus onto the trigger opens its tooltip at once, calling onOpen with 'trigger-focus'; moving it off closes the tooltip, calling onClose with 'trigger-focus' | The browser moves focus; Base UI opens |
Escape | While the tooltip is open, from anywhere in the document: calls onClose(event, 'escape-key', details), and focus stays on the trigger | Base UI |
Enter, Space on trigger | Run the trigger's own action, and close the tooltip with 'trigger-press' while closeOnClick is on | The browser, the trigger; Base UI |
No other key: the popup is out of the tab order (tabindex="-1") and holds no control, so Tab passes it by.
Tooltip adds no ARIA of its own, neither role="tooltip" on the popup nor aria-describedby or aria-label on the trigger, leaving the trigger named by its own words, as Base UI's guidelines ask. Perron's triggers already carry their names (an IconButton's required label, a Button's text), so a description that repeats the name would be read twice; aria-describedby would point at a popup that exists only while open, so the description would come and go with the hover; and the contract's first checklist line holds because the tooltip's words match the trigger's name, which the Default story checks. A word that says more than the name belongs in visible text or a popover, as the Do and don't table says.
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
trigger | The caller's element's own, such as button | Its own content or aria-label, such as IconButton's label | Its own, such as disabled; none added |
positioner | presentation, from Base UI | none | none |
root | none: a <div> that reads as its text | none: its children are read as text | none |
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the tooltip's words match the trigger's accessible name, which the trigger carries whether the tooltip is open or not; the popup draws no mark.
- Every landmark and named region has an accessible name: none here; the popup is not a landmark or a region.
- Every default label can be overridden: none here; Tooltip owns no strings.
- Every shape the type accepts renders, in every variant:
childrenrenders a string, an element and a list alike, as theContentstory shows. - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; the popup leaves the document when it closes, and Base UI makes the positionerinertwhile it is closed. - Native semantics first: the trigger is the caller's native
<button>or Base UI button, and the popup a plain<div>for words, with no role. - Focus moves in reading order and returns on close: the popup never takes focus, so opening and closing it leaves focus on the trigger.
- Every target is at least
size.target-minsquare: the trigger, the caller's control, such as IconButton'ssize.controlsquare; the popup takes no press. - Loops stop when the system asks for less motion: none here; the popup has no loop and no transition.
Owned strings
No owned strings: every word in the popup comes from the caller, as children, and the trigger's name from the trigger.
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | must | Give an icon-only control a tooltip with the same words as its accessible name: an IconButton labelled "Zoom in" and the tooltip "Zoom in" | Write tooltip words that differ from the trigger's name: a screen reader hears only the name | review |
| DD-2 | must | Keep the words to a short label, in sentence case | Put a sentence the reader needs in a tooltip: touch and screen reader users never see it, so put it in visible text or a popover | review |
| DD-3 | must | Pass a control that takes a ref and spreads its props as trigger, such as a Button, an IconButton or a ToolbarButton | Pass a component that drops the props it is given: Base UI's hover and focus handlers never reach the element, and the tooltip never opens | review |
| DD-4 | must | Keep a disabled trigger focusable with focusableWhenDisabled, so its tooltip still opens from the keys | Put the tooltip on a natively disabled button: it leaves the tab order, so the keyboard can't reach the tooltip | review |
| DD-5 | must | Put a button in a toolbar in a tooltip with trigger={<ToolbarButton … />}, the way round Toolbar gives | Pass the tooltip's trigger to the button's render, as for popups | review |
| DD-6 | should | Keep the app inside one PerronProvider, so a row of tooltips opens at once after the first | Add a provider per tooltip: each waits the full delay again | review |
| DD-7 | must | Use MapTooltip for a card against a point on the map | Wrap the map canvas in a Tooltip: it has one trigger, the whole canvas, not the vehicle under the pointer | review |
| DD-8 | must | Put only words in the tooltip | Put a link, a button or a field in it: the popup is out of the tab order, so the keys can't reach them | review |
| DD-9 | should | Change the side through slotProps.positioner, such as side: 'bottom' | Move the popup with a margin or a transform: Base UI's placement and flipping no longer match what is drawn | review |