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

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.

Highlight
Fig 1.1 · Tooltip · Dev view
Dev

Code anatomy

Slot Code slot Semantic
trigger trigger consumer-provided
container container tooltip
arrow arrow presentational
Both

Variants, properties, states

Variants

Structurally different versions of the component.

standardinline-help

Properties

The same component, parameterised.

PropertyType
sidetop | right | bottom | left
alignstart | center | end
delayinstant | short | base | long
arrowboolean

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

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-tooltip>` host element that wires the trigger via `id` reference and renders a `[role=tooltip]` container; positioning via Floating UI or CSS anchor positioning; arrow as a slot or pseudo-element attributes (`variant="inline-help"`, `side="top"`, `align="center"`, `delay="base"`, `arrow`); `data-state="open|closed|opening|closing"` for CSS
React Radix `Tooltip.Root` / `Tooltip.Trigger` / `Tooltip.Portal` / `Tooltip.Content` compositing the slots; positioning via Floating UI; React Aria `useTooltip` provides an alternative props with class-variance-authority for variant; `side`, `align`, `sideOffset`, `delayDuration` props on `Tooltip.Content`; `data-state` exposed
Angular (signals) Angular CDK `cdkTooltip` directive plus `OverlayModule` for positioning; signal-based `[uiTooltip]` content input; consumer wires it on focusable elements input<'standard' | 'inline-help'>(), input<'top' | 'right' | 'bottom' | 'left'>(); `[delay]` input for the show-delay
Vue third-party (Radix Vue, Headless UI does not ship Tooltip as of 2026-04, vue-floating); custom composable `useTooltip` wires aria-describedby plus pointer/focus listeners defineProps with literal-union types; `:side`, `:align`, `:delay`, `:arrow` props
Both

Events

  1. openChange
    Payload
    Boolean. `true` after the tooltip has finished entering (delay completed and visible), `false` after it has finished exiting. Mostly internal — consumers rarely observe tooltip open/close events because the surface is non-interactive, but the event exists for analytics ("tooltip view rate") and for orchestrating linked tooltips in instructional flows.
    Web Components
    `openChange` CustomEvent on the host with `event.detail = { open: boolean }`.
    React
    `onOpenChange(open: boolean)` controlled-pattern callback (Radix Tooltip exposes this on `Tooltip.Root`).
    Angular Signals
    `output<boolean>('openChange')`; rarely consumed.
    Vue
    `@update:open` for `v-model:open` if controlled; usually the tooltip is uncontrolled and this event is unused.
Dev

Performance thresholds

  • openDelaypointer-or-focus-hold700ms

    Default open delay before the tooltip reveals. Below ~200ms tooltips feel jumpy on mouse-traversal across multiple triggers; above ~1500ms they feel laggy when the user hovers intentionally. Radix and React Aria converge on ~700ms as the `base` delay; the canonical `delay` enum maps to this scale (instant=0, short=200, base=700, long=1500).

  • closeDelaypointer-leave-grace100ms

    Brief grace period after pointer-leave before dismiss, to prevent flicker when the user briefly moves the cursor across a child element of the trigger (e.g. an icon inside a wrapping button). 100ms is the canonical convention; any higher and the dismiss feels delayed.

Both

Accessibility

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

Accessibility acceptance

Keyboard walk

KeysExpected
Tab (focus enters trigger)Trigger receives focus; tooltip reveals after the configured delay. The trigger remains focused — Tab does NOT move focus into the tooltip (tooltip is non-interactive).
Tab (focus on trigger, tooltip open)Focus moves to the next focusable in the document (NOT into the tooltip). Tooltip closes (focus leaves the trigger).
Escape (focus on trigger, tooltip open)Tooltip dismisses persistently; pointer-leave or focus-leave does not re-reveal it until focus leaves the trigger and returns. APG canonical behaviour to allow keyboard users to suppress the tooltip when it obscures content.
Enter or Space (focus on trigger)Activates the trigger's primary action (the tooltip is supplementary; activating the trigger does not interact with the tooltip). Some triggers (an abbreviation `<abbr>`) have no primary action; Enter is a no-op.

Screen-reader announcements

TriggerExpected
Trigger receives focus, tooltip opensSR announces the trigger's accessible name followed by the tooltip text via `aria-describedby`. Verbose-mode-off users may hear the description as a separate announcement; verbose mode reads it inline with the trigger announcement.
Trigger pointer-hovers (no focus)Pointer-only users see the tooltip visually; SR users without focus on the trigger receive no announcement (no focus, no announcement). This is correct — the tooltip is supplementary, not essential.
Tooltip dismissed by EscapeTooltip disappears visually; SR users hear no announcement (the description simply unwires from aria-describedby for the duration of the focus session).

axe-core rules to assert

  • aria-tooltip-name
  • aria-required-attr
  • aria-allowed-role
  • color-contrast
  • role-img-alt
Dev

Common mistakes

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

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

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

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

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

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.