---
title: "Tooltip"
description: "A short supplementary label that appears when a pointer rests on a control or a keyboard moves focus to it, for information a reader can do without."
url: "https://opsinjs.pensievelabs.org/components/tooltip"
source: "https://opsinjs.pensievelabs.org/components/tooltip.md"
section: "Components"
status: "shipped"
kind: "component"
category: "overlay"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["hover label"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="tooltip"
  status="shipped"
  questions="[
  &#x22;No contrast pair has been measured: footnote ink on the card fill, and the arrow's fill against whatever surface sits behind it, are argued from the roles only.&#x22;,
  &#x22;Nobody has listened to a tooltip in a screen reader, so whether the aria-describedby label is announced as a description, or at all on touch, is unheard.&#x22;,
  &#x22;Nothing stops a caller putting the one fact a control cannot be used without behind a hover a phone reader never triggers; no lint catches it.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="tooltip" />

## Installation [#installation]

<ComponentInstall name="tooltip" unbuilt="false" importPath="@/components/ui/tooltip" dependencies="[&#x22;@base-ui/react&#x22;]" />

## Usage [#usage]

```tsx
import { Tooltip } from "@/components/ui/tooltip"
```

```tsx
<Tooltip content="A rolling mean across the nights you have logged.">
  <button type="button">Overnight average</button>
</Tooltip>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Expanding an icon-only control's name, already carried through aria-label.&#x22;,
  &#x22;Adding a short optional note beside a label that already stands on its own.&#x22;,
  &#x22;Surfacing a value's full form on demand, where the reading is already on screen.&#x22;,
]"
  avoid="[
  { case: &#x22;The label carries something a touch reader needs. A phone has no hover, so the tooltip never opens for them.&#x22;, instead: &#x22;callout&#x22; },
  { case: &#x22;You are explaining what a clinical word means. That belongs inline, in the accessibility tree.&#x22;, instead: &#x22;term&#x22; },
  { case: &#x22;You are attaching a hint to a form control, such as a date format. A field hint is always visible.&#x22;, instead: &#x22;field&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="tooltip"
  parts="[
  {
    name: &#x22;Tooltip&#x22;,
    describes: &#x22;The root. Owns open state, delay and portal.&#x22;,
    prop: &#x22;content&#x22;,
  },
  {
    name: &#x22;Tooltip.Trigger&#x22;,
    describes: &#x22;The caller's own focusable element, keeping its own aria-label.&#x22;,
    prop: &#x22;children&#x22;,
  },
  {
    name: &#x22;Tooltip.Popup&#x22;,
    describes: &#x22;A raised card at the footnote step, portalled against the preferred side.&#x22;,
    prop: &#x22;side&#x22;,
  },
  {
    name: &#x22;Tooltip.Arrow&#x22;,
    describes: &#x22;A rotated square in the popup's card fill.&#x22;,
  },
]"
/>

<CompositionTree
  name="tooltip"
  tree="[
  {
    part: &#x22;Tooltip&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;Provider plus root&#x22;,
    children: [
      {
        part: &#x22;Tooltip.Trigger&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;data-slot=\&#x22;tooltip-trigger\&#x22;&#x22;,
      },
      {
        part: &#x22;Tooltip.Popup&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;data-slot=\&#x22;tooltip-popup\&#x22;, portalled&#x22;,
        children: [
          {
            part: &#x22;Tooltip.Arrow&#x22;,
            cardinality: &#x22;1&#x22;,
            note: &#x22;data-slot=\&#x22;tooltip-arrow\&#x22;&#x22;,
          },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### On an icon button [#on-an-icon-button]

Each icon button carries its own name through `aria-label`; `content` repeats it
visibly. The labels name fictional actions.

<ComponentPreview name="tooltip-on-an-icon-button" kind="example" align="start" />

### A supplementary hint [#a-supplementary-hint]

The setting is named in plain words, so a touch reader needs no hover, and
`content` adds one optional sentence.

<ComponentPreview name="tooltip-a-supplementary-hint" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Keep a tooltip to a phrase in sentence case, with no terminal punctuation unless
it is a full sentence. Write what the control does or means, never an
instruction that only appears on hover.

<DoDont>
  <DoDont.Do>
    **"Reminders wait until the morning during the hours you set here"** beside a
    labelled setting.
  </DoDont.Do>

  <DoDont.Dont>
    **The only label on an icon-only control, shown on hover alone.** A touch
    reader sees a bare glyph.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

Audited against WCAG 2.2 AA: a source pass over the whole rubric, and a rendered
pass over the measured findings. Author-run, not independent; clinical review
is still pending. The rendered pass found no axe violation, no target-size
breach, no focus-visible gap and no clipping at 320px. The source pass found
nothing to change in `tooltip.tsx`.

What holds:

* Base UI wires the popup through `aria-describedby`, so the label is a
  description, never a replacement for the control's own name.
* Focus opens the tooltip, Escape dismisses it without moving focus, and the
  label follows focus off the trigger.
* No status colour, no colour literal, no ramp step: `footnote` step, no `px`
  size, neither colour axis.

Open:

* A tooltip is unreachable on a hover-less touch device, so `content` stays
  supplementary.
* The popup's non-text contrast (SC 1.4.11) is the shared `border-border` role
  token, reasoned rather than measured.
* The popup staying hoverable across the gap (SC 1.4.13) is a Base UI 1.7.0
  default, not a manual trace.

<KeyboardTable
  name="tooltip"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Moves focus onto the trigger and shows the tooltip&#x22;,
    notes: &#x22;No open delay on focus.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Moves focus off the trigger and hides the tooltip&#x22;,
    notes: &#x22;The label follows focus.&#x22;,
  },
  {
    keys: &#x22;Escape&#x22;,
    action: &#x22;Dismisses the tooltip&#x22;,
    notes: &#x22;Focus stays on the trigger and reopens it on the next pointer rest.&#x22;,
  },
  {
    keys: &#x22;Enter, Space&#x22;,
    action: &#x22;Activates the underlying control&#x22;,
    notes: &#x22;The tooltip binds neither key.&#x22;,
  },
]"
/>

<ContrastReport component="tooltip" />

## API reference [#api-reference]

<PropsTable name="TooltipProps" />

`content` is required; an empty one raises a development warning. `children`
must be focusable. `side` defaults to `top` and Base UI flips it at the screen
edge. `delay` defaults to 600 and governs the pointer only; focus opens the
tooltip at once.

## Related [#related]

* [Term](./term.mdx) attaches a plain-English meaning to a clinical word inline.
* [Field](./field.mdx) carries a hint that is part of a form control and always visible.
* [Callout](./callout.mdx) sets essential information apart on the surface.
