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

Includes archived items
import { Tooltip, Button } from '@design-schema/react';

<Tooltip content="Includes archived items"><Button label="Items" variant="secondary" /></Tooltip>

Props

6 rows
PropTypeDefaultDescriptionPlatforms
content requiredstring—

The tooltip text. One short phrase or sentence; no markup, no links, no line breaks.

web, lit, rn, swiftui
children requiredcontent—

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).

web, lit, rn, swiftui
placementtop | bottom | start | endtop

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.

web, lit, rn, swiftui
describesbooleantrue

`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.

web, lit, rn, swiftui
openboolean—

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.

web, lit, rn, swiftui
delaydefault | nonedefault

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).

web, lit, rn, swiftui

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

triggerpopuptext

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.

Tooltip keyboard support

1 row
KeyAction
EscapeHides the tooltip without moving focus. (tooltip visible)

Contrast pairs

1 row
ForegroundBackgroundLevel
color.inverse.foregroundcolor.inverse.surfaceAA

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.