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 '@cronocode/react-box/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>

Keyboard and pointer

InputResult
Pointer rests on the triggerShows under the trigger after openDelay (300 ms by default).
Pointer leaves the triggerHides after closeDelay (150 ms) — unless it lands on the tooltip.
Pointer moves onto the tooltipStays open for as long as it is there.
Tab to the triggerShows immediately: focus ignores openDelay.
Tab awayHides, unless the pointer is still on the trigger.
EscapeHides. Focus stays on the trigger, and it does not come back until the pointer leaves and returns.
Tab, againReaches the next control — the tooltip is never in the tab order.

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
last reason:
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). adjustTranslateX and adjustTranslateY nudge where it lands.
JSX
<Tooltip content="On brand" bgColor="indigo-600" color="white" borderRadius={2} adjustTranslateY="4px">
  {(trigger) => <Button {...trigger}>Delete</Button>}
</Tooltip>

Only need the positioning?

A tooltip is a pattern; escaping an overflow: hidden is not. The portal-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 '@cronocode/react-box/components/overlay';

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