# Tooltip

A short label on hover and on focus.

- Group: Overlays
- Import: `import { Tooltip } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/tooltip/
- Related: dialog

## When to reach for it

Never for anything the reader NEEDS: a tooltip is unreachable on touch and invisible while scanning.

## Anatomy

- **Provider** (required) — TooltipProvider, wrapped once around the app or the smallest subtree that has tooltips. It holds the shared 700ms open delay and the 300ms skip window; Radix throws without it rather than rendering an untimed tip.
- **Trigger** (required) — children, handed to Radix with asChild — so the child IS the trigger and no wrapper is inserted around it.
- **Tip** (required) — content, in the portalled panel: 11px mono on --feature-surface, capped at 16rem, 6px off the chosen side, at --z-toast (300) so it stays above a modal it was opened inside.
- **Portal** — Where the tip lands — document.body, or the element an enclosing OverlayContainer names, which is also the box it collides against with 8px of padding.

## Best practices

### Do

- Put the Provider high and put one there. Radix throws without it, and a provider per tooltip defeats the shared timing that stops a row of icon buttons flashing a separate tip on every hover.
- Match the tip to the control’s aria-label word for word: two different names for one control is the “label in name” failure (WCAG 2.5.3), and a voice-control user says the words they can see.
- Keep the tip to a phrase. It is capped at 16rem and set in 11px mono, so a sentence wraps into a five-line block that covers the thing it was describing.
- Put it on a control that already works without it: Radix returns early when the pointer type is touch, so the tip never opens on a phone at all and anything it is the sole carrier of is simply missing there.

### Don’t

- delayDuration={0} does not just make it faster — the state becomes instant-open rather than delayed-open, and the fade is keyed to delayed-open, so the tip appears with no transition at all.
- Setting delayDuration on one Tooltip overrides the provider for that trigger alone, which is how a toolbar ends up with one tip that appears instantly beside neighbours at 700ms — read as lag, not as emphasis.
- Nothing focusable belongs in content: the tip is not in the tab order and closes when the trigger loses focus, so a link or a button in there is reachable by pointer and by nothing else. That is a Popover.

## Accessibility

- Inside a bounded frame — a device preview, an embedded console — wrap the subtree in `<OverlayContainer container={el}>`. The panel then renders into that element and collides with its edges instead of the viewport’s, and inherits the `dir` and `data-density` set there.
- The trigger is asChild, so the child must be focusable — a div trigger simply has no keyboard tooltip, which this API shape makes obvious rather than silent.
- Not an accessible name. An icon-only button still needs its own aria-label.

## Keyboard

- Tab — Shows the tip — focus reveals it, not only hover.
- Escape — Dismisses it.

## TooltipProvider

Re-export of `TooltipPrimitive.Provider`.

Wrap the app — or the smallest subtree that has tooltips — once. Radix needs it to share the open/close timing between neighbouring triggers, which is what stops a row of icon buttons flashing a tooltip per hover.

## Tooltip

A short label on hover and on focus. `asChild` on the trigger by design: the tooltip must not add a wrapper that swallows the trigger's own focus ring or breaks a flex row. It also means the child has to be focusable — a `<div>` trigger gets no keyboard tooltip, which is the failure this API shape makes obvious rather than silent. Not a replacement for an accessible name. An icon-only button still needs its own `aria-label`; the tooltip repeats that name for sighted pointer users.

### Props

- `children` (required) — `ReactNode`. The element the tooltip describes. Must be focusable.
- `content` (required) — `ReactNode`. The tip. Keep it to a phrase — a tooltip is unreachable on touch and invisible to a reader who is scanning, so anything a user NEEDS belongs on the page instead.
- `side` — `ComponentProps<typeof TooltipPrimitive.Content>['side']` default `'top'`.
- `sideOffset` — `number` default `6`.

Also accepts: `Pick<ComponentProps<typeof TooltipPrimitive.Root>, 'open' | 'defaultOpen' | 'onOpenChange' | 'delayDuration'>`.

## Example — an icon button

```tsx
import { Button, Tooltip, TooltipProvider } from '@misoto22/folio'

) => clearTimeout(timer)
  }, [copied])

  return (
<TooltipProvider>
  <div className="flex items-center gap-3">
    {/* One string for the tip and the name, so the two cannot drift apart
        — and both change with the icon, because a copy control that looks
        identical before and after leaves the reader clicking it twice. */}
    <Tooltip content={label}>
      <Button iconOnly aria-label={label} variant="secondary" onClick={() => setCopied(true)}>
        {copied ? (
          <RiCheckLine size={16} className="text-(--ok)" aria-hidden />
        ) : (
          <RiFileCopyLine size={16} aria-hidden />
        )}
      </Button>
    </Tooltip>

    <Tooltip content="Share this frame" side="bottom">
      <Button iconOnly aria-label="Share this frame" variant="secondary">
        <RiShareLine size={16} aria-hidden />
      </Button>
    </Tooltip>
  </div>
</TooltipProvider>
```

## Example — one provider

```tsx
import { Button, Tooltip, TooltipProvider } from '@misoto22/folio'

<TooltipProvider>
  <div className="flex items-center gap-1 rounded-(--radius) border border-(--rule) p-1">
    {TOOLS.map((tool) => (
      <Tooltip key={tool.name} content={tool.name}>
        <Button iconOnly aria-label={tool.name} variant="ghost" size="sm">
          <tool.icon size={16} aria-hidden />
        </Button>
      </Tooltip>
    ))}
  </div>
</TooltipProvider>
```

## Example — a shortcut hint

```tsx
import { Button, Kbd, Text, Tooltip, TooltipProvider } from '@misoto22/folio'

<TooltipProvider>
  <div className="flex flex-col items-center gap-4">
    <div className="flex items-center gap-3">
      <Tooltip content="Save the draft" side="bottom">
        <Button keycap="S">Save draft</Button>
      </Tooltip>
      <Tooltip content="Publish this post" side="bottom">
        <Button variant="secondary">Publish</Button>
      </Tooltip>
    </div>
    <Text size="sm" tone="muted">
      The same keys are printed on the page, so <Kbd>S</Kbd> is not news a
      hover has to break.
    </Text>
  </div>
</TooltipProvider>
```
