Tooltip
Show a short visual label on hover or keyboard focus.
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use Tooltip to show an icon-only control’s name or keyboard shortcut. It is visual only: it does not open on touch (unless you turn on longPress) and screen readers do not read it. Give the control its own accessible name and keep essential information visible. For an explanation people can open, use a Popover with openOnHover. For a link preview, use a Hover Card.
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@youj/react'
import { ArchiveIcon } from '@youj/icons'
export function ArchiveButton() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<Button variant="ghost" size="icon" aria-label="Archive" />}>
<ArchiveIcon />
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}Give an icon-only trigger an aria-label that matches the tooltip text. The tooltip doesn't name or describe its trigger, so the label is what assistive technology reads.
Description
A formatting toolbar whose buttons show a name and keyboard shortcut, with neighbors opening at once after the first.
Wrap a toolbar, or your whole app, in one TooltipProvider. The first tooltip waits for the pointer to rest (500ms here, Base UI's delay). Once one is showing, its neighbors open at once and skip their entrance, so sweeping across a toolbar reads as one continuous label. Moving on within 400ms (the provider's timeout) keeps the group warm. Try it on the formatting toolbar above.
Tooltips can hold a Kbd or a KbdGroup. The keycaps pick up the tooltip's colors in both modes.
A tooltip on each side of its trigger.
side picks the edge (top by default) and align the alignment along it. The tooltip drifts 3px in from the trigger, so its motion always points back at the control it names, and it flips when the preferred side runs out of room. Logical sides, inline-start and inline-end, follow the writing direction.
A disabled button that still explains itself in a tooltip.
A disabled button that explains itself should stay focusable, or keyboard users never reach the reason. Pass focusableWhenDisabled to the Button and let it take pointer events back so hover works too. Presses still do nothing. The tooltip isn't announced, so the example also points aria-describedby at the same sentence for screen readers.
Code
tooltip-demo.tsx, the Demo story. The component itself comes from the package.
Reference
PropsTooltipProvider passes through to Base UI's provider (delay, which…
Props
TooltipContent
TooltipContent adds no props of its own. They pass through to TooltipPrimitive.Popup.Props, Pick<TooltipPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'collisionPadding'>.
TooltipProvider passes through to Base UI's provider (delay, which defaults to 500 here, closeDelay and timeout). Tooltip takes the root props (open, defaultOpen, onOpenChange, disabled) and longPress, and TooltipTrigger takes the trigger's (delay, closeDelay, closeOnClick, render).
TouchTouch has no hover, so a tooltip doesn't open there
Touch
Touch has no hover, so a tooltip doesn't open there. A tap is a press, and Base UI opens a tooltip on focus only when the focus comes from a keyboard, so a tap never leaves one stuck on screen. Anything a tooltip says must therefore reach people another way: an icon-only control needs an aria-label, and anything people need in order to act (why a control is disabled, a limit, a warning) belongs in visible text next to the control. On a phone, prefer a visible label under an icon when there's room for one.
longPress on Tooltip adds what Android does for icon buttons: holding the trigger for half a second shows the tooltip, and that press doesn't activate the control. The tooltip hides 1.5 seconds after the finger lifts or at the next touch anywhere, and a finger that moves, as in a scroll, cancels it. Keyboard shortcuts inside it are left out on touch. It helps someone who wonders what an icon means; it doesn't replace the label.
<Tooltip longPress>
<TooltipTrigger render={<Button variant="ghost" size="icon" aria-label="Archive" />}>
<ArchiveIcon />
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>MotionShow, hide, neighbor in a group, keyboard focus, Escape… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
Show3px drift from the trigger, scale .97 → 1, fade (yj-tooltip) | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Hide | duration.instantease.exit80ms · cubic-bezier(0.4, 0, 1, 1) | 80mscubic-bezier(0.4, 0, 1, 1) | |
Neighbor in a group, keyboard focus, EscapeBase UI sets data-instant | no token 0ms | 0ms | |
| Open delayhover intent, not motion: Speed leaves it alone | no token 500ms in a provider | 500ms in a provider | |
| Long press (touch, `longPress`)how long to hold, then how long it stays after the finger lifts | no token 500ms, 1.5s | 500ms, 1.5s |
Reduced motion
Tooltips appear and disappear at once, with no drift. Delays are unchanged.
Reduced motion
Tooltips appear in place, without the drift or the fade, and hide at once. Hover delays stay the same, because they measure intent rather than animate anything.
AccessibilityKeyboard, ARIA, focus
Accessibility
- Tooltips are visual only. Base UI doesn't describe the trigger with them, so the trigger's own name (its text or
aria-label) must carry the meaning. - Keyboard focus opens the tooltip without waiting for the delay. Hover opens it after the delay.
- They don't open on touch, since there's no way to reveal them before tapping. With
longPress, holding the trigger shows one, and that press doesn't activate the control.
| Key | Action |
|---|---|
Tab | Focusing a trigger shows its tooltip. |
Escape | Hides the tooltip and leaves focus on the trigger. |