# Tooltip

_Box Kite 2.2.5 · a markdown copy of https://www.box-kite.dev/tooltip/_

A description that appears on hover and on focus, wired to its trigger with aria-describedby — the APG tooltip, including the three WCAG 1.4.13 rules everyone forgets.

**Import**

```jsx
import Tooltip from '@box-kite/react/components/tooltip';
```

**Usage**

```jsx
<Tooltip content="Deletes the row for good">
  {(trigger) => <Button {...trigger}>Delete</Button>}
</Tooltip>
```

## Why the trigger is a render prop

The tooltip has to put `aria-describedby` on the element the user lands on — the control itself, not a wrapper around it. Cloning a child would mean guessing where that attribute goes, and this library has two answers: Box takes DOM attributes in a `props` bag, a plain `<button>` takes them at the top level. So the trigger is handed to you instead, and you say where it belongs. It carries a `ref` too — the bubble is positioned against the trigger's own box, which is what keeps it under the control and out of the layout.

```jsx
// A Box component — ref and props are both top-level Box props, so one spread does it.
<Tooltip content="Save the draft">{(trigger) => <Button {...trigger}>Save</Button>}</Tooltip>

// A plain element — the same two pieces, named.
<Tooltip content="Save the draft">{(trigger) => <button ref={trigger.ref} {...trigger.props}>Save</button>}</Tooltip>
```

## What the component guarantees

Dismissible — WCAG 1.4.13

Escape closes the tooltip and focus does not move. It stays closed while the pointer sits where it is: re-showing it because nothing moved would put the user straight back where they were.

Hoverable — WCAG 1.4.13

Leaving the trigger starts a `closeDelay` (150 ms) rather than closing, so the pointer can travel onto the tooltip — to read a long description, or to select text in it. Once it is on the tooltip, the tooltip keeps it open.

Persistent — WCAG 1.4.13

Nothing hides it on a timer. It closes when the pointer leaves and focus has moved on, and not before.

**Controlled, with the reason it changed**

```jsx
const [open, setOpen] = useState(false);

<Tooltip
  content="Deletes the row for good"
  open={open}
  onOpenChange={(next, { reason }) => {
    setOpen(next);
    console.log(reason); // 'hover' | 'focus' | 'pointer-leave' | 'blur' | 'escape'
  }}
>
  {(trigger) => <Button {...trigger}>Delete</Button>}
</Tooltip>
```

## Styling

Every Box prop on `<Tooltip>` styles the bubble, on top of the built-in `tooltip` component style (inverted against the page, so it reads as an overlay in either theme). Where it lands is `side`, `align` and `offset` — the placement vocabulary `Overlay` and `useAnchorPosition` share — and a side with no room flips to its opposite on its own.

```jsx
<Tooltip content="On brand" bgColor="indigo-600" color="white" borderRadius={2} side="end" offset={2}>
  {(trigger) => <Button {...trigger}>Delete</Button>}
</Tooltip>
```

## Only need the positioning?

A tooltip is a pattern; escaping an `overflow: hidden` is not. The layer-and-position half of this component ships on its own as `Overlay` — no ARIA, no open state, no dismissal — and that is what to reach for when the thing being rendered is not a description of a control. Before A3 this component _was_ that primitive, so code that used it to escape an overflow becomes `Overlay`, unchanged.

```jsx
import Overlay from '@box-kite/react/components/overlay';

<Overlay p={3} bgColor="slate-800">anything, anywhere</Overlay>
```

## Tooltip props

Everything below is this component’s own. All 235 of Box’s style props work on it too, and those are on [/box](https://www.box-kite.dev/box.md) rather than repeated here.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `content` | `React.ReactNode` | — | The description itself. Nothing renders while this is empty. |
| `children`required | `(trigger: TooltipTrigger) => React.ReactNode` | — | The trigger, handed the ref and props that wire it to the tooltip. |
| `open` | `boolean` | — | Controlled open state. Leave it out and the tooltip owns it. |
| `defaultOpen` | `boolean` | `false` | Whether it starts open, when the tooltip owns its own state. |
| `onOpenChange` | `ChangeHandler<boolean, TooltipReason>` | — | Fires with the new state and why it changed — `'hover'`, `'focus'`, `'pointer-leave'`, `'blur'` or `'escape'`. |
| `openDelay` | `number` | `300` | How long the pointer has to rest on the trigger, in ms. Default 300 — sweeping across a toolbar should not light every control up. Focus ignores it: a keyboard user asked for the tooltip by arriving. |
| `closeDelay` | `number` | `150` | The grace period after the pointer leaves, in ms. Default 150, so the pointer can reach the tooltip (WCAG 1.4.13). |
| `side` | `AnchorSide` | `'bottom'` | Which side of the trigger the bubble sits on — `top`/`bottom` are the block axis, `start`/`end` the inline one. Default `'bottom'`, and a side with no room flips to its opposite either way. |
| `align` | `AnchorAlign` | `'center'` | Which of the trigger's edges to line the bubble up with along the other axis. Default `'center'`. |
| `offset` | `number` | `1` | The gap between trigger and bubble, on the ÷4 spacing scale. Default 1 — 4px, which is near enough for the pointer to cross without leaving the trigger (WCAG 1.4.13) and far enough to read as a layer. |
| `flip` | `boolean` | `true` | Whether a side with no room may be swapped for its opposite. Default `true`. |

## Tooltip keyboard

| Key | Result |
| --- | --- |
| `Tab` | Shows it as soon as the trigger has focus, ignoring `openDelay`: a keyboard user asked for it by arriving. |
| `Escape` | Hides it, leaving focus on the trigger. It stays hidden until the pointer or focus leaves and comes back. |
| `Tab, again` | Reaches the next control, and the tooltip closes behind it. |

### Pointer

| Key | Result |
| --- | --- |
| `Resting on the trigger` | Shows it after `openDelay`, 300 ms by default: sweeping across a toolbar should not light every control up. |
| `Leaving the trigger` | Hides it after `closeDelay`, 150 ms — unless the pointer lands on the tooltip itself. |
| `Moving onto the tooltip` | Keeps it open for as long as the pointer is there, so a long description can be read or selected. |

## Tooltip accessibility

Implements [the WAI-ARIA APG pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/).

- `role="tooltip"` on the bubble and `aria-describedby` on the trigger, so the description is read as part of the control rather than as text that happens to be nearby.
- WCAG 1.4.13, all three parts: Escape dismisses it without moving focus, the pointer can travel onto the bubble and read it, and nothing hides it on a timer.
- The bubble is never in the tab order — a focusable tooltip is a dialog with the wrong role — and it renders nothing at all while `content` is empty.

Swept with axe on every release, in 2 states: `Tooltip`, `Tooltip (open)`. No violations, with contrast and landmark rules left to a human. Screen-reader results are not published yet.

## Tooltip style tree

Every part the component draws is a node with a name, so a default can be restyled with `Box.components()` instead of a selector — and a variant is a name too.

`tooltip`variants: closed

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

