# 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 ``. 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 `
` 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['side']` default `'top'`. - `sideOffset` — `number` default `6`. Also accepts: `Pick, 'open' | 'defaultOpen' | 'onOpenChange' | 'delayDuration'>`. ## Example — an icon button ```tsx import { Button, Tooltip, TooltipProvider } from '@misoto22/folio' ) => clearTimeout(timer) }, [copied]) return (
{/* 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. */}
``` ## Example — one provider ```tsx import { Button, Tooltip, TooltipProvider } from '@misoto22/folio'
{TOOLS.map((tool) => ( ))}
``` ## Example — a shortcut hint ```tsx import { Button, Kbd, Text, Tooltip, TooltipProvider } from '@misoto22/folio'
The same keys are printed on the page, so S is not news a hover has to break.
```