Designer view

Tooltip

A non-interactive floating panel that surfaces brief descriptive text about its trigger, revealed on hover or focus and dismissed on blur, pointer-leave, or Escape. Distinct from Popover (interactive) and Modal (blocking): tooltip content is read-only, the user never enters the tooltip, and Tab moves past the trigger without entering the tooltip body. Used for icon labels, abbreviation expansions, short helper text — never for essential information.

Also called Tip

When to use

Use

When a control's accessible name benefits from a brief supplementary description that does not need to be permanently visible — icon-only buttons, abbreviations, status badges, column headers in dense tables. The description is non-essential (the control operates correctly without it) and short (one or two sentences).

Avoid

For interactive content (buttons, links, forms inside the floating surface) — that is `Popover`. For essential information that defines the control — that belongs in the accessible name (`aria-label`, visible text). For paragraph-length explanations — that is body prose or a Disclosure. For status messages that announce state changes — that is `Toast` or a `aria-live` region.

Versus related

  • popover

    `Popover` is interactive — the user may tab into the body and click controls. `Tooltip` is non-interactive — focus never enters the tooltip, and the body must contain only descriptive text. The boundary is canonical and APG- defined; do not blur it with "tooltip with a button" designs.

  • modal

    `Modal` is blocking and viewport-centred; `Tooltip` is non-blocking and trigger-anchored. They occupy opposite ends of the floating-surface spectrum (Modal: maximum modality, content-rich; Tooltip: zero modality, description-only).

  • alert

    `Alert` is an inline `aria-live` message announcing state changes; `Tooltip` is on-demand description revealed by hover or focus. Alert pushes content at the user; Tooltip is pulled by the user's interaction.

  • menu-button

    `Tooltip` is descriptive text on a single trigger and carries no actions; `MenuButton` opens a list of interactive commands invoked on click or keyboard. They differ in role, dismiss-contract, and content rules — a tooltip with a list of links inside is a MenuButton in disguise.

  • toast

    `Toast` is system-pushed transient notification floating above content; `Tooltip` is user-pulled description anchored to a trigger. Toast appears without a hover or focus event; tooltip never appears unsolicited.

  • code-block

    `Code Block` is the standalone surface displaying verbatim code (block or inline `<code>`); `Tooltip` is the on-hover / on-focus description anchored to an inline `<code>` reference. Tooltip on inline `<code>` is the canonical pattern for explaining what a code reference means without taking users out of flowing prose; Code Block is the surface where the actual code lives. They pair across documentation contexts — Code Block carries the example, Tooltip carries per-token explanations when authored.

Tooltip is a hover- or focus-revealed descriptive label — supplementary information that helps the user understand a control without changing the surface. It is non-interactive: Tab does not move focus into it, Escape dismisses it persistently, and pointer-leave or focus-leave hides it. The reference documents the canonical reveal-and-hide delays, the aria-describedby contract that wires the trigger to the tooltip text, the divergence from Popover (interactive), and the platform-default delay values that production implementations diverge on.

Highlight
Fig 1.1 · Tooltip · Designer view

Implementations

How specific libraries realise the canonical anatomy. Each entry records the deltas between the canon and the library's surface.

radix Tooltip
import * as Tooltip from '@radix-ui/react-tooltip';
// Provider must wrap the app (or the subtree) once — typically at root level.
// delayDuration overrides the 700ms default; skipDelayDuration controls the
// grace window for moving between adjacent triggers without re-triggering delay.
<Tooltip.Provider delayDuration={700} skipDelayDuration={300}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<IconButton aria-label="Settings" />
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="top" align="center" sideOffset={6}>
Adjust settings
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>

Divergence

From Type → To Rationale
anatomy[container] reshaped Tooltip.Root + Tooltip.Portal + Tooltip.Content (with VisuallyHidden role=tooltip child) The canonical container is one slot bearing both `role="tooltip"` and the floating surface. Radix splits this across four layers: Root owns controlled state and the delay timer, Portal relocates the DOM node to document.body (avoiding stacking-context clipping), Content owns the visible floating surface, and — critically — the `role="tooltip"` is applied to a VisuallyHidden child inside Content, not to Content itself. This means the visible content can be styled freely while the accessible description node is always present in the DOM regardless of CSS. Consumers composing the canonical anatomy must use all four together. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 480–530 (TooltipContentImpl), verified 2026-05-31.
anatomy[trigger] reshaped Tooltip.Trigger (renders <button> by default; ignores touch pointerType) Canonical trigger is consumer-provided — the tooltip attaches listeners to whatever element the consumer wires. Radix's Tooltip.Trigger renders a `<button>` by default and explicitly skips `pointerType === 'touch'` in its onPointerMove handler (source line ~310: `if (event.pointerType === 'touch') return`). This means Radix Tooltip does NOT open on touch pointer-move — touch users must rely on the focus event path (tap → focus → instant-open via onFocus handler). The canonical mismatch "hover-only" is partially addressed via focus, but touch pointerType is explicitly excluded from the pointer-move reveal path. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 303–316, verified 2026-05-31.
axes.properties[side] renamed side on Tooltip.Content — values: top | right | bottom | left Canonical uses CSS logical property terms (block-start, inline-end, block-end, inline-start) to be direction-agnostic and RTL-aware. Radix uses physical direction values (top / right / bottom / left) inherited from the underlying @radix-ui/react-popper primitive (Floating UI). In RTL layouts this means `side="right"` anchors visually right regardless of text direction — the consumer must reverse the value manually for RTL. Default: "top" (not canonical "block-end"). Source: https://www.radix-ui.com/primitives/docs/components/tooltip (Content API table), verified 2026-05-31.
axes.properties[delay] reshaped delayDuration (number, ms) on Tooltip.Provider and Tooltip.Root Canonical models delay as an enum (instant=0, short=200, base=700, long=1500). Radix exposes raw milliseconds via `delayDuration` on both Provider (global default) and Root (per-instance override). There is no built-in enum mapping — design systems layered on Radix must implement the canonical enum → ms translation themselves. Default: 700ms (matches canonical `base`). Provider also adds `skipDelayDuration` (default 300ms) which has no canonical equivalent: it is the grace window after closing one tooltip within which the next tooltip opens instantly without delay. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 44–62 (TooltipProviderProps), verified 2026-05-31.
axes.properties[arrow] reshaped Tooltip.Arrow — optional child component, not a boolean prop Canonical models the arrow as a boolean prop on the tooltip surface. Radix implements it as a composable child component (Tooltip.Arrow) that the consumer includes or omits. This means the arrow's presence, size, and className are entirely consumer-controlled; there is no library-level default. The canonical `arrow: true` default is achievable by always rendering Tooltip.Arrow inside Tooltip.Content. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 555–574 (TooltipArrow), verified 2026-05-31.
axes.variants[inline-help] omitted Radix Tooltip has no variant concept at all — it is an unstyled primitive. The canonical `inline-help` variant (icon + tap-to-toggle on mobile, distinct visual treatment) is a design-system layer above Radix. Consumers must compose Tooltip.Root with a custom trigger and CSS to achieve the inline-help visual behaviour. Source: https://www.radix-ui.com/primitives/docs/components/tooltip (no variant prop listed), verified 2026-05-31.
axes.states reshaped data-state: closed | delayed-open | instant-open (no opening/closing states) Canonical defines four data states: closed, opening, open, closing. Radix collapses this to three: `closed`, `delayed-open` (opened after the delayDuration timer), and `instant-open` (opened immediately, e.g. via focus or when skipDelayDuration is in effect). There is no separate `closing` state — the tooltip transitions directly from open to closed when the dismiss trigger fires, leaving exit animation timing entirely to CSS `[data-state="closed"]` selectors. The `opening` state is also absent; the delay phase is not reflected in `data-state` (it remains `closed` until the timer fires). Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 189–191 (stateAttribute useMemo), verified 2026-05-31.
axes.properties[side] extended + Tooltip.Provider: required wrapping component with no canonical equivalent. Props — `delayDuration` (number, default 700), `skipDelayDuration` (number, default 300), `disableHoverableContent` (boolean, default false). The Provider manages a shared delay state so that moving the pointer between multiple triggers within `skipDelayDuration` ms opens each instantly. The `disableHoverableContent` flag opts out of the WCAG 2.1 SC 1.4.13 grace area behaviour (by default Radix implements a pointer-transit polygon that keeps the tooltip open while the pointer moves from trigger to content). The Provider pattern is Radix-specific: it coordinates delay state across sibling tooltips, enabling the "skip delay on quick traversal" UX. The canonical spec has no equivalent coordination primitive — each tooltip is independent. The `disableHoverableContent` flag controls the WCAG 2.1 SC 1.4.13 hoverable-content requirement (content stays open when the pointer moves over it); Radix implements this by default via a triangular grace polygon computed from the trigger and content rects. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 29–109 (TooltipProvider + TooltipContentHoverable), verified 2026-05-31.
motion.durations reshaped data-state CSS attribute selectors on Tooltip.Content; no bundled duration tokens Radix ships no motion durations or easing values. Consumers write CSS transitions/animations on `[data-state="delayed-open"]`, `[data-state="instant-open"]`, and `[data-state="closed"]` using their own design tokens. The canonical `motion.duration.fast` (open) and `motion.duration.instant` (close) are achievable but entirely consumer-side — the library exposes the state attribute, not timing. Source: https://www.radix-ui.com/primitives/docs/components/tooltip (Animation section), verified 2026-05-31.
motion.reducedMotionFallback omitted Radix does not implement a reduced-motion behaviour. The data-state attribute still flips on open/close, but no `@media (prefers-reduced-motion)` suppression is bundled. Consumers must apply this CSS rule themselves. The canonical `instant` fallback is achievable at the consumer CSS layer but is not part of the primitive. Source: https://www.radix-ui.com/primitives/docs/components/tooltip (no reduced-motion docs), verified 2026-05-31.
events[openChange] renamed onOpenChange (Tooltip.Root prop) One-to-one match in contract. `onOpenChange(open: boolean)` on Tooltip.Root fires when the open state changes. The canonical event payload contract holds. Note: fires on both open and close transitions; does NOT distinguish between `delayed-open` and `instant-open`. Source: github.com/radix-ui/primitives packages/react/tooltip/src/tooltip.tsx lines 153–163 (useControllableState onChange), verified 2026-05-31.
Why this audit reads the way it does

Radix Tooltip (@radix-ui/react-tooltip v1.2.8) is a low-level, unstyled primitive that correctly implements the canonical accessibility contract (aria-describedby via VisuallyHidden role=tooltip, focus-trigger via onFocus, Escape dismiss, no interactive content) but diverges substantially on structure and axis shape. The dominant structural divergence is the required Tooltip.Provider wrapper, which introduces cross-tooltip delay coordination with no canonical equivalent. The anatomy is split across Root/Portal/Content/Arrow rather than a single container slot, and the ARIA role is applied to a VisuallyHidden inner node rather than the visible surface — an implementation detail invisible to consumers but load-bearing for the aria-describedby contract. Axis divergences: physical direction values instead of logical ones (RTL caveat), raw milliseconds instead of the canonical delay enum, and Tooltip.Arrow as a composable child instead of a boolean prop. The data-state model collapses the canonical four-state graph to three values (no separate opening/closing), omitting the intermediate states that exit animations depend on in the canonical motion spec. Touch behaviour: Radix explicitly skips pointerType=touch in the pointer-move handler, relying on the focus event for touch reveals. This matches the canonical "tap-to-focus" pattern for touch but means no hover-path reveal on touch; the inline-help variant's tap-to-toggle behaviour is entirely absent as Radix ships no variants.

react-aria Tooltip
import {
TooltipTrigger,
Tooltip,
OverlayArrow,
Button,
} from 'react-aria-components';
// Standard usage — hover + focus, 1500 ms warmup delay, OverlayArrow
<TooltipTrigger delay={1500} closeDelay={500}>
<Button aria-label="Edit">
<PencilIcon />
</Button>
<Tooltip placement="top" offset={6}>
<OverlayArrow>
<svg width={8} height={8} viewBox="0 0 8 8">
<path d="M0 0 L4 4 L8 0" />
</svg>
</OverlayArrow>
Edit this item
</Tooltip>
</TooltipTrigger>
// Focus-only (keyboard or programmatic focus, no hover)
<TooltipTrigger trigger="focus">
<Button>Save</Button>
<Tooltip placement="bottom">Saves all changes immediately</Tooltip>
</TooltipTrigger>
// Controlled open state
<TooltipTrigger isOpen={isOpen} onOpenChange={setIsOpen}>
<Button>Info</Button>
<Tooltip>Additional information about this field</Tooltip>
</TooltipTrigger>

Divergence

From Type → To Rationale
anatomy[trigger] reshaped First child of <TooltipTrigger> — wrapped in FocusableProvider internally; aria-describedby is applied by useTooltipTrigger and forwarded via FocusableProvider rather than wired by the consumer on the element directly The canonical `trigger` slot requires the consumer to wire the element and attach the `aria-describedby` relationship themselves. React Aria inverts this: `<TooltipTrigger>` is a compound parent that wraps the trigger child in an internal `FocusableProvider`, applies `aria-describedby` automatically through `useTooltipTrigger`, and manages the hover/focus event listeners. The consumer provides the element as a React child — not as a ref or id reference — so the wiring is implicit rather than explicit. The structural shape is a parent-wraps-trigger tree rather than a sibling-plus-wire model. Source: packages/react-aria-components/src/Tooltip.tsx (fetched 2026-05-31); packages/react-aria/src/tooltip/useTooltipTrigger.ts (fetched 2026-05-31)
anatomy[container] renamed Tooltip The canonical anatomy names the floating tooltip panel the `container` slot. React Aria names the corresponding component `<Tooltip>`. It renders a `<div>` with `role="tooltip"` (applied by `useTooltip` in packages/react-aria/src/tooltip/useTooltip.ts), is portalled via `<OverlayContainer>` (renders into document.body by default), and handles positioning through `useOverlayPosition`. The semantic function is identical; only the component name differs. Source: packages/react-aria-components/src/Tooltip.tsx (fetched 2026-05-31); packages/react-aria/src/tooltip/useTooltip.ts (fetched 2026-05-31)
anatomy[arrow] renamed OverlayArrow The canonical `arrow` slot is a decorative tip indicator. React Aria ships `<OverlayArrow>` — a shared sub-component also used by Popover and Dialog — rather than a tooltip-specific arrow element. `<OverlayArrow>` renders its consumer-provided SVG children and receives `arrowProps` (position styles) and `placement` from the parent `<Tooltip>` via context. The consumer must supply the SVG path for the triangle; React Aria does not ship a default triangle glyph. The component is optional but it is a named sub-component, not a CSS pseudo-element or slot prop. Source: packages/react-aria-components/src/Tooltip.tsx — OverlayArrowContext (fetched 2026-05-31); https://react-aria.adobe.com/Tooltip (fetched 2026-05-31)
axes.variants[inline-help] omitted React Aria Tooltip ships no `variant` prop or `inline-help` concept. The library is behaviour-only; visual distinctions between standard and inline-help tooltip styles are a consumer responsibility (class names, CSS custom properties, or a wrapper component). No API surface in `TooltipTriggerComponentProps` or `TooltipProps` corresponds to the canonical `variant` axis. Source: packages/react-aria-components/src/Tooltip.tsx — TooltipProps interface (fetched 2026-05-31)
axes.properties[side] reshaped placement: 'top' | 'bottom' | 'left' | 'right' | 'top left' | 'top right' | ... (physical Placement type) The canonical `side` property uses logical CSS direction values (`block-start`, `inline-end`, `block-end`, `inline-start`) that are RTL-aware. React Aria's `placement` prop on `<Tooltip>` uses physical direction names (`top`, `bottom`, `left`, `right`) inherited from the `Placement` type in `react-aria/useOverlayPosition`. There is no logical- direction variant in the type definition. RTL handling for placement is not automatic — consumers must supply `left`/`right` or swap them manually in RTL contexts. The prop default is `'top'`. Source: packages/react-aria-components/src/Tooltip.tsx — placement default comment (fetched 2026-05-31)
axes.properties[align] reshaped crossOffset: number (default 0) — numeric pixel offset on the cross axis The canonical `align` property is a semantic enum (`start`, `center`, `end`) controlling alignment along the perpendicular axis. React Aria exposes no alignment enum; instead it provides `crossOffset: number` (a pixel offset from the default centered position) on `<Tooltip>`. Achieving `start` or `end` alignment requires calculating the appropriate pixel offset relative to the trigger dimensions, which the consumer must do manually. The `offset` prop controls main-axis distance (analogous to canonical `sideOffset`, not to `align`). Source: packages/react-aria-components/src/Tooltip.tsx — TooltipInner useOverlayPosition call (fetched 2026-05-31)
axes.properties[delay] reshaped delay: number (default 1500 ms) — numeric milliseconds on TooltipTrigger The canonical `delay` property is a semantic enum (`instant`, `short`, `base`, `long`) that maps to library-specific millisecond values. React Aria exposes `delay` as a raw number prop (milliseconds) on `<TooltipTrigger>` with a hardcoded default of `1500` ms (`TOOLTIP_DELAY = 1500` in `useTooltipTriggerState`). Focus reveals always bypass the delay and show immediately (the `open(immediate: true)` path in `useTooltipTrigger`). The canonical semantic enum is absent; consumers work with raw millisecond values. Source: packages/react-stately/src/tooltip/useTooltipTriggerState.ts — TOOLTIP_DELAY constant and delay prop default (fetched 2026-05-31)
axes.properties[arrow] reshaped Presence of <OverlayArrow> child inside <Tooltip> — optional child component, not a boolean prop The canonical `arrow` axis is a boolean prop (`arrow: boolean`) that toggles the arrow slot. React Aria has no arrow boolean prop on `<Tooltip>`. The arrow is rendered by including an `<OverlayArrow>` element as a child of `<Tooltip>` — its presence or absence is structural (the consumer adds or omits the JSX element), not declarative (a prop on the parent). There is no `showArrow`, `hasArrow`, or equivalent boolean on `<Tooltip>` or `<TooltipTrigger>`. Source: packages/react-aria-components/src/Tooltip.tsx — OverlayArrowContext (fetched 2026-05-31)
axes.properties extended + `closeDelay: number` (default 500 ms) on `<TooltipTrigger>` — duration after pointer-leave before the tooltip hides when dismissing via hover. Keyboard blur dismisses immediately (`immediate = true`), bypassing `closeDelay`. Maps to the `TOOLTIP_COOLDOWN = 500` constant in `useTooltipTriggerState`. The canonical `performance.closeDelay` threshold documents a 100 ms grace period as the canonical convention, but there is no `closeDelay` prop in the canonical axes. React Aria makes this a first-class prop with a 500 ms default (5× the canonical suggestion) to match Spectrum design guidelines. The difference matters: at 500 ms the tooltip lingers noticeably longer than the canonical 100 ms convention, which reduces flicker on dense trigger layouts but increases perceived latency to dismiss. Source: packages/react-stately/src/tooltip/useTooltipTriggerState.ts — TOOLTIP_COOLDOWN constant and closeDelay prop default (fetched 2026-05-31)
axes.properties extended + `trigger: 'hover' | 'focus'` on `<TooltipTrigger>` (default `'hover'`). When set to `'focus'`, the tooltip only opens on keyboard focus — hover events are suppressed in `onHoverStart`. The default `'hover'` mode opens on both hover (after delay) and focus (immediately). The canonical axes have no `trigger` mode prop — the canonical contract mandates reveal on both pointer-enter and focus always. React Aria adds a focus-only mode for cases where hover reveal is undesirable (e.g. on controls that already have hover styling), making hover suppression explicit rather than requiring the consumer to override event handlers. This is a React Aria extension without a canonical equivalent. Source: packages/react-aria/src/tooltip/useTooltipTrigger.ts — trigger === 'focus' guard in onHoverStart/onHoverEnd (fetched 2026-05-31)
axes.properties extended + `shouldCloseOnPress: boolean` (default `true`) on `<TooltipTrigger>`. When `true`, pressing or clicking the trigger immediately closes the tooltip. Set to `false` to keep the tooltip visible when the trigger is activated (e.g. a toggle button that should preserve the tooltip). The canonical axes have no equivalent press-dismiss toggle. React Aria adds it to handle the edge case where the trigger's primary action and the tooltip reveal are intentionally co-present. Without this prop the only option would be to manage open state in a controlled pattern. Source: packages/react-aria/src/tooltip/useTooltipTrigger.ts — shouldCloseOnPress destructure (fetched 2026-05-31)
axes.states.data[opening] reshaped Global warm-up timer shared across all TooltipTrigger instances on the page; once any tooltip has opened (globalWarmedUp = true), subsequent tooltips open immediately without the delay. After the last tooltip closes, a cool-down period (max(TOOLTIP_COOLDOWN, closeDelay) = 500 ms) resets globalWarmedUp to false. The canonical `opening` state is per-instance: the delay timer fires for each individual trigger. React Aria implements a singleton global warm-up state (`globalWarmedUp`, `globalWarmUpTimeout`, `globalCooldownTimeout` module-level variables in `useTooltipTriggerState`) that all tooltip instances share. This matches Spectrum UX guidelines: hovering between multiple tooltips quickly should feel instant after the first reveal. The canonical does not model this cross-instance warm-up / cool-down dynamic. Source: packages/react-stately/src/tooltip/useTooltipTriggerState.ts — globalWarmedUp, globalWarmUpTimeout, globalCooldownTimeout (fetched 2026-05-31)
axes.states.interactive[hover] reshaped Touch interactions do not open the tooltip. The hover handler guards with `getInteractionModality() === 'pointer'` so touch-based hover events are suppressed. Tooltips are only available via pointer (mouse/stylus) hover or keyboard focus in React Aria. The canonical states that touch devices use long-press (~500 ms) as the canonical fallback and the inline-help variant switches to tap-to-toggle automatically. React Aria explicitly does not show tooltips on touch screen interactions (documented on https://react-aria.adobe.com/Tooltip and enforced via the modality guard in `useTooltipTrigger`). Touch users on mobile browsers receive no tooltip, contrasting with the canonical long-press fallback model. This is a deliberate library choice to avoid unreliable touch-hover simulation. Source: packages/react-aria/src/tooltip/useTooltipTrigger.ts — getInteractionModality() === 'pointer' guard (fetched 2026-05-31); https://react-aria.adobe.com/Tooltip (fetched 2026-05-31)
events[openChange] reshaped onOpenChange(isOpen: boolean) on <TooltipTrigger>, not on <Tooltip>. Also available on standalone <Tooltip> (via OverlayTriggerProps) when used without a TooltipTrigger. The canonical `openChange` event is defined at the component level without specifying which sub-component owns it. React Aria places `onOpenChange` on `<TooltipTrigger>` (via `TooltipTriggerProps` extending `OverlayTriggerProps`) because the open/close logic is managed by `useTooltipTriggerState` in the trigger — not in the `<Tooltip>` itself. The `<Tooltip>` also accepts `isOpen` / `defaultOpen` directly for standalone use (without `<TooltipTrigger>`), in which case `onOpenChange` is available on the `<Tooltip>`. The callback signature is `(isOpen: boolean) => void` — identical to the canonical payload. Source: packages/react-stately/src/tooltip/useTooltipTriggerState.ts — TooltipTriggerProps extends OverlayTriggerProps (fetched 2026-05-31)
Why this audit reads the way it does

React Aria Tooltip is a behaviour-only compound primitive split across two components — `<TooltipTrigger>` (owns delay, warm-up, open state, and event wiring) and `<Tooltip>` (owns positioning, portal, role="tooltip", and animation). The visual anatomy (tokens, variant styles, arrow SVG) is entirely the consumer's responsibility. The five deepest divergences from the canon are: 1. Global warm-up / cool-down singleton — unlike the canonical per-instance delay model, React Aria implements a module-level shared timer (globalWarmedUp) so that hovering between multiple triggers feels instant after the first 1500 ms warmup. This is a structural divergence: the canonical opening/closing states are per-tooltip; in React Aria they are governed by a cross-instance state machine. 2. Touch suppression — the canonical documents long-press as the touch fallback. React Aria explicitly does NOT show tooltips on touch interactions (modality guard in useTooltipTrigger). Touch users on mobile receive no tooltip. 3. Physical placement vs logical side — placement uses physical names (top/bottom/left/right) rather than the canonical logical direction values (block-start/inline-end/block-end/inline-start). RTL symmetry must be handled by the consumer. 4. delay is numeric ms (default 1500) not a semantic enum — the canonical delay enum (instant/short/base/long) is absent. React Aria's default of 1500 ms matches the canonical `long` value, not the `base` 700 ms that the canonical performance section recommends as the default. 5. closeDelay default 500 ms vs canonical 100 ms — React Aria's post-hover dismiss grace period is 5× the canonical convention, intentionally matching Spectrum guidelines at the cost of slower perceived dismiss on dense UIs. The rename surface (isDisabled, isOpen, defaultOpen, onOpenChange, trigger, delay, closeDelay, shouldCloseOnPress) follows React Aria's is*/on*/allows* naming scheme. The ARIA contract (role="tooltip", aria-describedby wired automatically, no focus entry into tooltip) is fully canonical-compliant.

Designer

Figma anatomy

Slot Figma type Hint
trigger instance Consumer-provided trigger; in Figma the Tooltip frame is anchored to but does not contain the trigger
container frame Floating frame with optional tip; max-inline-size cap
arrow rectangle 6×6 triangle clipped from a rotated square
Designer

Token usage per slot

container
spacing
  • paddingspacing.tight
radius
  • cornerradius.sm
color
  • backgroundcolor.surface.sunken
  • foregroundcolor.text.primary
  • bordercolor.border.subtle
elevation
  • shadowelevation.md
typography
  • sizetext.sm
  • lineHeightleading.snug
arrow
color
  • backgroundcolor.surface.sunken
  • bordercolor.border.subtle
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps standard / inline-help.
SideEnumsideblock-start / inline-end / block-end / inline-start (logical, RTL-aware). Authored placement preference; auto-flips when colliding with viewport.
AlignEnumalignstart / center / end along the perpendicular axis.
DelayEnumdelayinstant / short / base / long. Maps to library-specific milliseconds (Radix uses 0/200/700/1500 by default).
Has ArrowBooleanarrowToggles the arrow slot. True by default for standard variant; false for inline-help.
ContentTextchildrenThe tooltip text. Capped at one to two short sentences by canon.
Designer

Motion

TransitionDuration token
openmotion.duration.fast
closemotion.duration.instant
Easing
motion.easing.decelerate
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smAt and below, hover-driven tooltips are unreliable because most touch devices do not surface persistent hover events. The canonical fallback is long-press (~500ms) plus focus on tap-and-hold; some implementations replace tooltip with a contextual Popover triggered on tap. Inline-help variant switches to tap-to-toggle automatically (the icon becomes a button with aria-haspopup).
breakpoint.mdAbove this width, hover and focus reveal as authored. The floating positioning honours `side` and `align`; auto-flip and shift work as designed.
Both

Internationalisation

RTL · mirroring

Side property uses logical-direction values (block-start / inline-end / block-end / inline-start) — `inline-end` flips from right (LTR) to left (RTL) automatically; `inline-start` mirrors. Align is logical (start/center/end follow inline direction). Tooltip text inherits document direction; mixed-direction text (Arabic content with English code snippets in the tooltip) follows the inner spans' `dir` attributes. Auto-flip is symmetric across directions. Arrow glyph is a triangle — direction-neutral.

Text expansion

Tooltip text expansion is constrained by the canonical max-inline-size (commonly 240–320px). Long-text languages (German, Russian, Finnish) may wrap to two or three lines; above three lines, the canonical mistake `tooltip-paragraph- length` applies — the content belongs in a Popover or body prose. Allow soft-wrap; never truncate with ellipsis (the truncated description loses meaning).

Both

Variants, properties, states

Variants

Structurally different versions of the component.

standard inline-help

Properties

The same component, parameterised.

PropertyType
side block-start | inline-end | block-end | inline-start
align start | center | end
delay instant | short | base | long
arrow boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visible
data
closedopeningopenclosing
Both

State transitions

FromToTrigger
closedopeningPointer enters the trigger or focus moves to the trigger. The configured delay timer starts; the tooltip is not yet visually revealed.
openingopenThe delay timer completes (or, under prefers-reduced-motion reduce, the tooltip appears immediately at the canonical instant fallback). aria-describedby is wired and the SR-announceable description is present.
openclosingPointer leaves the trigger AND focus is not on the trigger; or focus leaves the trigger AND pointer is not over it; or the user presses Escape to dismiss persistently. The canonical OR-conjunction prevents flicker when the user moves focus while hovering.
closingclosedThe exit animation completes (or immediately under reduced motion). aria-describedby remains on the trigger but the container is removed from the DOM (or hidden via CSS).
Both

Figma↔Code mismatches

  1. 01
    Figma

    A tooltip drawn with a button inside ("Got it" or "Learn more")

    Code

    A tooltip with role=tooltip, which APG forbids interactive content

    Consequence

    Designers may treat tooltip as a small popover and add dismiss buttons or links. Implementations that follow the Figma file create something with `role="tooltip"` plus a button — SR users cannot reach the button (focus does not enter tooltips), and the visual affordance suggests interaction that does not work. The pattern collapses to Popover.

    Correct

    Treat the boundary as canonical: tooltips contain only non-interactive content. If the surface needs a dismiss button or any interaction, redesign as a Popover. Document this distinction prominently in the canonical reference and in `whenToUse`.

  2. 02
    Figma

    A tooltip drawn that only appears on hover

    Code

    A tooltip that fires on hover but not on focus

    Consequence

    Designers think tooltips are a hover affordance; developers shipping hover-only break keyboard accessibility — keyboard users tab to the trigger but never see the description. Touch devices fall back to long-press; without focus-trigger they have no canonical reveal at all on touch keyboards.

    Correct

    Tooltip MUST trigger on both pointer-enter and focus. Document this as a non-negotiable a11y rule in the canonical reference. Touch devices use long-press by convention; the reveal happens via the same focus event pattern when the trigger is focusable.

  3. 03
    Figma

    Tooltip drawn carrying essential information

    Code

    Tooltip used as the primary descriptor for a control

    Consequence

    Designers may use tooltip text as the only descriptor for icon-only controls; developers shipping this lose the description for SR users (who hear the icon's `aria-label` only) and for users on slow connections (who may not see the tooltip render before activating). The essential information is locked behind a hover-only reveal.

    Correct

    Essential information is in the trigger's accessible name (`aria-label`, visible text, etc.), never in the tooltip. The tooltip supplements with details that are useful but not required to operate the control. Document the canonical contract: tooltip is supplementary description, not primary label.

  4. 04
    Figma

    A tooltip drawn pointing at a passive area (a label, a static word)

    Code

    A tooltip wired to a non-focusable element with `aria-describedby`

    Consequence

    Designers anchor tooltips to non-interactive surfaces; developers wire them with no focus-trigger because the element is not focusable. Keyboard users miss the tooltip entirely; pointer users see it on hover but the experience is incomplete.

    Correct

    If the element needs a tooltip, it must be focusable — either it's already an interactive control, or wrap it in a focusable element (a button, a `<dfn>` or `<abbr>` with `tabindex="0"`). The tooltip's reveal contract requires both pointer and keyboard reachability.

Both

Contracts

Non-negotiable contracts

  1. APGAPG: Tooltip pattern

    Tooltip body contains only descriptive text — no buttons, links, form controls, or other interactive content. Focus never enters the tooltip.

    Tooltips with interactive content violate the canonical contract and break keyboard / SR access — the user can see the content but cannot reach it. Reach for Popover when interaction is needed.

  2. APGAPG: Tooltip pattern

    Reveal on hover OR focus; dismiss on pointer-leave AND focus-leave OR Escape.

    Hover-only triggers exclude keyboard users from the description entirely (WCAG 2.1.1 violation). Most-often broken contract in real-world implementations.

Designer

Common mistakes

Blocker

#tooltip-with-interactive-content

Tooltip containing buttons or links

Problem

The tooltip body has interactive elements (a "Learn more" link, a "Got it" button). APG forbids this — tooltip content is non-interactive, focus does not enter the tooltip, and SR users cannot reach the interactive children.

Fix

If the surface needs interaction, use a Popover. The distinction is canonical: Tooltip = non-interactive descriptive text; Popover = interactive contextual content. No "tooltip with a button" middle ground.

Blocker

#tooltip-no-focus-trigger

Tooltip only reveals on pointer hover

Problem

The tooltip fires on `mouseenter` but not on `focus` events. Keyboard users tab to the trigger; the tooltip never appears. Touch devices that surface the focus event also lose the tooltip.

Fix

Always pair pointer-enter with focus-enter as reveal triggers, and pointer-leave with focus-leave as dismiss triggers (with the OR-conjunction documented in the transitions block — tooltip stays open while either pointer or focus remains).

Blocker

#tooltip-essential-info

Essential information in tooltip

Problem

The tooltip text is the only descriptor for an icon-only control. SR users who skip the tooltip via verbose-mode-off get just the icon's `aria-label`; pointer users on slow connections may activate before the tooltip renders.

Fix

Essential info goes in the trigger's accessible name. The tooltip supplements with non-essential details. The test: remove the tooltip — does the control still operate correctly with full meaning? If no, the tooltip held essential info that belongs in the label.

Major

#tooltip-pointer-events-block

Tooltip captures pointer events from underlying content

Problem

The tooltip's container has `pointer-events: auto` — the user's mouse enters the tooltip area on its way to another control, the tooltip blocks the click, and the trigger cannot be reached again. Or worse, the tooltip itself becomes hoverable and traps the cursor in a hover loop.

Fix

Tooltip container has `pointer-events: none` by default. The tooltip is a non-interactive overlay that text can be read from but not clicked into. Pointer events pass through to the underlying content. The trigger handles all hover logic; the container is purely visual.

Minor

#tooltip-paragraph-length

Tooltip with multi-paragraph content

Problem

The tooltip body grows beyond a brief sentence — multiple sentences, lists, or paragraph-length prose. Hover dismisses while the user is reading; SR description fires the entire paragraph as one announcement.

Fix

Cap tooltip content at one or two short sentences. Anything longer should live in body prose, a Popover, or a Disclosure. Document the canonical length convention and enforce it in design review.

Accessibility hints
Slot Accessibility hint
trigger Trigger element carries `aria-describedby` referencing the tooltip container's id (NOT `aria-labelledby` — the tooltip is supplementary description, not the accessible name). For triggers without their own accessible name (e.g. an IconButton), the tooltip text duplicates as the `aria-label` plus the `aria-describedby` reference, so SR users hear the label first then the description.
container Apply `role="tooltip"`. The container has no interactive children — buttons, links, form controls inside a tooltip violate the APG contract. Sized to text; do not host layouts. The container does NOT receive focus; tabindex is absent.
arrow Decorative; do not put `role` on it. The trigger-to-tooltip relationship is communicated by `aria-describedby`, not by the arrow.