Tooltip
A short text label that appears on hover or focus to name or explain a control — never the only place information lives, and never interactive.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Tooltip } from '@design-schema/react'; Examples
import { Tooltip, Button } from '@design-schema/react';
<Tooltip content="Includes archived items"><Button label="Items" variant="secondary" /></Tooltip>Props
6 rows| Prop | Type | Default | Description |
|---|---|---|---|
| content required | string | — | The tooltip text. One short phrase or sentence; no markup, no links, no line breaks. |
| children required | content | — | Exactly one focusable element (a Button, Link, Input). The tooltip attaches to it; a non-focusable child is an error, because keyboard users could never see the tooltip. Accessibility: The child must be focusable so hover and focus are equivalent (WCAG 1.4.13, 2.1.1). |
| placement | top | bottom | start | end | top | Preferred side; flips when it would overflow the viewport (on native, measured with measureInWindow like Popover). `start`/`end` are logical and mirror in right-to-left writing. |
| describes | boolean | true | `true`: the tooltip is supplementary and becomes the child's accessible description (aria-describedby). `false`: the tooltip IS the child's name (an icon-only button whose label equals the tooltip) and is linked as aria-labelledby instead — set this when the child has no visible text and its `label` equals `content`, to avoid announcing it twice. When they differ anyway, the tooltip text wins and the child's visible label is no longer its accessible name; no development warning is emitted for it. |
| open | boolean | — | Controlled visibility, for stories and tests only (the Keyboard story renders the tooltip open with it). Product code never sets it: a tooltip is hover and focus driven. There is no change event: Escape still hides a tooltip rendered with `open: true`, and it stays hidden until the `open` prop next changes. While `open` is set, only Escape and changes to `open` affect visibility; hover, focus, blur and long-press do not. |
| delay | default | none | default | Hover delay before showing: `default` uses `motion.duration.base` × 3 (roughly 600ms, so casual mouse movement does not flash tooltips); `none` for toolbars where a sibling tooltip is already open (a shared "warm" state so moving along a toolbar shows tooltips instantly: after a tooltip hides, siblings show with no delay for one motion.duration.base, the `warmWindow` constant — and warm skips the delay for `default` tooltips too, while `none` is always instant; the pointer may cross to the tooltip within one motion.duration.fast before it hides). |
Accessibility
Decisions this component leaves to you
Required props that carry accessibility meaning: children. Each is described in the props table above.
Implicit ARIA role: tooltip.
Anatomy
What generation guarantees
Escape dismisses — Escape closes it from anywhere inside.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.
No hover-only content — Nothing is reachable by hover alone; keyboard focus reveals the same content.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.