Skip to main content

Tooltip

A tooltip is the trigger's accessible description: a few words that say what an icon button does or expand an abbreviation. Interactive content belongs in a Popover. Positioned with @floating-ui/react (flip, shift, autoUpdate) and portalled to <body>.

import Tooltip from '@zuilib/components/tooltip'

Basic

The child is cloned with the tooltip's ref, hover / focus handlers and aria-describedby merged in. Its own ref, handlers and aria-describedby are composed, not replaced. The trigger must be keyboard focusable: a Button or a link. A plain <span>, <div> or <svg> child gets tabIndex={0} automatically.

Loading example

Placement and arrow

placement takes any Floating UI Placement; the tooltip flips to the other side when there is no room. arrow draws a small triangle that fills with the same --tooltip-surface colour as the bubble, and the arrow height is added to the offset automatically.

Loading example

Delay and disabled

delay applies to hover only; keyboard focus opens at once. A single number sets both open and close, an object sets each. disabled never opens and closes an open tooltip, while the trigger keeps rendering.

Loading example

Controlled and render-prop trigger

open / onOpenChange control the state. Pass a function as children to spread the trigger props yourself; spread all of them onto one focusable element.

Loading example

Recolouring

The bubble is --foreground on --background inverted so it reads over any surface. The colour goes through --tooltip-surface, which the arrow follows, so one class recolours both.

Loading example

Props

PropTypeDefaultDescription
contentrequiredReactNodeWhat the tooltip says; keep it short, it is the trigger's accessible description
childrenrequiredReactElement | (props: TooltipTriggerProps) => ReactNodeThe trigger. An element is cloned with the trigger props; a function receives them to spread onto one focusable element
placementPlacement'top'Floating UI placement (top, bottom-start, …); flips when there is no room
delaynumber | { open?: number; close?: number }{ open: 200, close: 0 }Hover delay in ms; keyboard focus opens at once
offsetnumber1.5 × --spacing (6px)Gap between trigger and panel in px; the arrow height is added automatically
arrowbooleanfalseDraw a small arrow pointing at the trigger
disabledbooleanfalseNever opens; closes an open tooltip. The trigger still renders
openbooleanControlled open state
defaultOpenbooleanfalseInitial state when uncontrolled
onOpenChange(open: boolean) => voidCalled on every open / close
portalbooleantrueRender under <body>. false keeps the panel inline so subtree token overrides and a subtree .dark reach it
classNamestringMerged last onto the panel. Set --tooltip-surface to recolour the bubble and arrow together
arrowClassNamestringMerged last onto the arrow svg
panelPropsOmit<HTMLAttributes<HTMLDivElement>, 'className' | 'children' | 'role' | 'id'>Extra panel attributes (style, data-*). The id is owned by the tooltip
refRef<HTMLDivElement>The panel element

TooltipTriggerProps (the render-prop argument) is the callback ref, the hover / focus / dismiss handlers, aria-describedby while open, and data-state.

Slots

SlotElementNotes
[data-slot="tooltip"]divThe panel. Also data-state="open", data-placement (resolved placement) and data-side (top of top-start)
[data-slot="tooltip-arrow"]svgPresent with arrow; fills with --tooltip-surface

The trigger element gets data-state="open" | "closed" and no slot of its own (it is the consumer's element).

/* Consumer CSS: rounder tooltips that slide from the side they sit on */
[data-slot="tooltip"] {
border-radius: var(--radius-lg);
}
[data-slot="tooltip"][data-side="bottom"] {
animation-name: slide-in-from-top;
}

Tokens

TokenUsed for
--foreground, --backgroundInverted bubble colours (via --tooltip-surface)
--radius-smCorner radius
--text-xsType size
--shadow-mediumPanel shadow
--spacingDefault offset (× 1.5) and viewport padding (× 2)
--duration-* via animate-fade-inMount fade, off under prefers-reduced-motion

Accessibility

  • The panel has role="tooltip"; while open the trigger's aria-describedby points at it, joined onto any description the trigger already had.
  • Opens on mouse or pen hover (after delay.open) and on keyboard focus (at once). Touch taps do not open it, so it can never stick on a phone.
  • Moving the pointer from the trigger onto the tooltip keeps it open, so its text can be hovered and selected (WCAG 1.4.13).
  • Closes on Escape, on pressing the trigger, and on an outside press. Escape is not swallowed: a surrounding Dialog closes on the same press.
  • The trigger must be focusable or keyboard and screen-reader users can never open it. Host <span> / <div> / <svg> children get tabIndex={0}; a custom component that mounts an unfocusable node logs a warning in development.
  • The content is a description: an icon button still needs its own aria-label for its name.
  • Popover: a floating panel with interactive content.
  • Button: the usual trigger; size="icon" with an aria-label.
  • Kbd: show a shortcut inside the tooltip content.