Bridge 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 · Bridge view
Both

Figma↔Code mismatches

Where designer and developer worlds typically misalign on this component.

  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

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

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps standard / inline-help.
SideVariantsidetop / right / bottom / left. Authored placement preference; auto-flips when colliding with viewport.
AlignVariantalignstart / center / end along the perpendicular axis.
DelayVariantdelayinstant / 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.
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).
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
Dev

Code anatomy

Slot Code slot Semantic
trigger trigger consumer-provided
container container tooltip
arrow arrow presentational
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.
Both

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

Internationalisation

RTL · mirroring

Side property is direction-neutral (top/right/bottom/left are physical). 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

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
Both

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.