Browse docs
Browse docs
A short read-only hint that surfaces on hover or keyboard focus. Perfect for icon-only buttons, truncated text, and any UI where the affordance isn't obvious at a glance.
import { Tooltip, IconButton } from '@dashforge/tw';
<Tooltip content="Delete row">
<IconButton aria-label="Delete" color="danger"><TrashIcon /></IconButton>
</Tooltip>Built on @radix-ui/react-tooltip. Renders role="tooltip", wires aria-describedby on the trigger, and shows on both hover AND keyboard focus so it's not a mouse-only affordance.
Same trio as <Popover> — pick by interaction richness:
Use <Tooltip> when... | Use <Popover> when... |
|---|---|
| The content is a read-only hint | The content is interactive (forms, buttons, links) |
| The trigger is a small icon-only element | The trigger opens a richer surface |
| The user shouldn't be able to click into it | The user needs to focus and interact |
If you find yourself putting a <Button> inside a <Tooltip>, you actually want a <Popover> — the tooltip won't focus-trap the button, and it'll auto-dismiss on hover-out.
import { Tooltip, IconButton, Stack } from '@dashforge/tw';
<Stack direction="row" gap={3}>
<Tooltip content="Delete record">
<IconButton aria-label="Delete" color="danger" variant="ghost">
<TrashIcon />
</IconButton>
</Tooltip>
<Tooltip content="This field syncs to the workspace-level policy.">
<IconButton aria-label="Info" variant="ghost">
<InfoIcon />
</IconButton>
</Tooltip>
</Stack>Even with a tooltip attached, <IconButton> still needs its own aria-label — the tooltip provides aria-describedby, not aria-label. Screen readers announce both.
import { Tooltip, IconButton } from '@dashforge/tw';
import { Trash2, Info } from 'lucide-react';
<Tooltip content="Delete record">
<IconButton aria-label="Delete" color="danger" variant="ghost">
<Trash2 size={16} />
</IconButton>
</Tooltip>
<Tooltip content="This field syncs to the workspace-level policy.">
<IconButton aria-label="Info" variant="ghost">
<Info size={16} />
</IconButton>
</Tooltip>side selects which edge of the trigger the tooltip attaches to. align further tunes the perpendicular axis.
import { Tooltip, Button } from '@dashforge/tw';
<Tooltip side="top" content="Top tooltip"> <Button>top</Button></Tooltip>
<Tooltip side="right" content="Right tooltip"> <Button>right</Button></Tooltip>
<Tooltip side="bottom" content="Bottom tooltip"> <Button>bottom</Button></Tooltip>
<Tooltip side="left" content="Left tooltip"> <Button>left</Button></Tooltip><Tooltip side="top" content="Top tooltip"> <Button>top</Button></Tooltip>
<Tooltip side="right" content="Right tooltip"> <Button>right</Button></Tooltip>
<Tooltip side="bottom" content="Bottom tooltip"> <Button>bottom</Button></Tooltip>
<Tooltip side="left" content="Left tooltip"> <Button>left</Button></Tooltip>Radix auto-flips the side if the requested edge would clip against the viewport.
delayDuration controls the hover-open latency in milliseconds. Default is 200 — small enough to feel responsive, large enough to avoid tooltip storms when a mouse skims across an icon toolbar.
<Tooltip delayDuration={0} content="Instant"> <Button>0ms</Button></Tooltip>
<Tooltip delayDuration={200} content="Default"> <Button>200ms</Button></Tooltip>
<Tooltip delayDuration={600} content="Patient"> <Button>600ms</Button></Tooltip>Set delayDuration={0} for critical hints (e.g. keyboard shortcut display on Cmd-hover). Keep the default for icon toolbars.
By default a small arrow indicator points from the tooltip toward the trigger. Hide it for tightly-packed toolbars where the arrow is visual noise:
<Tooltip hideArrow content="Clean look">
<IconButton aria-label="Settings"><Cog /></IconButton>
</Tooltip>Omit open/onOpenChange for the default hover/focus behavior. Provide them when you want to reveal the tooltip programmatically (e.g. onboarding tour, "did you know?" nudge):
const [open, setOpen] = useState(false);
useEffect(() => {
if (isFirstVisit) {
setOpen(true);
const t = setTimeout(() => setOpen(false), 4000);
return () => clearTimeout(t);
}
}, [isFirstVisit]);
<Tooltip open={open} onOpenChange={setOpen} content="👋 Try this shortcut!">
<Button>New feature</Button>
</Tooltip>Configure <Tooltip> defaults application-wide.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Tooltip: {
defaults: { side: 'top', align: 'center', delayDuration: 200, sideOffset: 4, hideArrow: false },
},
},
});Configurable axes (TooltipVariantProps):
| Axis | Type | Notes |
|---|---|---|
side | 'top' | 'right' | 'bottom' | 'left' | Preferred side. Auto-flips against the viewport. |
align | 'start' | 'center' | 'end' | Perpendicular alignment. |
hideArrow | boolean | Hide the arrow indicator. |
delayDuration | number | Hover-open latency in milliseconds. |
sideOffset | number | Pixel gap between trigger and tooltip. |
Non-visual axes (children, content, open, onOpenChange, slotProps) are per-instance — they carry the tooltip's identity or programmatic state.
Precedence chain (lowest → highest):
side='top', align='center', hideArrow=false, delayDuration=200, sideOffset=4).theme.components.Tooltip.defaults (application-wide).<Tooltip side="right" delayDuration={0} />) — wins over theme.slotProps.content — reaches the tooltip surface for a targeted class override.TypeScript: theme.components.Tooltip.defaults autocompletes to the five axes above.
Reactivity: useComponentDefaults('Tooltip') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The element that, when hovered/focused, shows the tooltip. |
content | ReactNode | — | Tooltip body content. |
align | 'start' | 'center' | 'end' | 'center' | Alignment along the perpendicular axis. |
delayDuration | number | 200 | Delay before the tooltip shows on hover (ms). |
hideArrow | boolean | false (arrow shown) | Hide the arrow indicator. |
onOpenChange | (open: boolean) => void | — | Called when the tooltip's open state changes. |
open | boolean | — | Controlled open state. Use when the tooltip should be programmatic (e.g. revealed on a manual button press). Omit for default hover / focus behavior. |
side | 'top' | 'right' | 'bottom' | 'left' | 'top' | Side of the trigger to show the tooltip on. |
sideOffset | number | 4 | Side offset in pixels from the trigger. |
slotProps | TooltipSlotProps | — | Per-slot overrides. |
sx | string | — | Shortcut for slotProps.content.className — Tailwind utility classes appended to the tooltip surface via tailwind-merge (later wins on conflicts). Same ergonomic pattern as <Dialog sx="...">.Use this for one-off overrides ( sx="bg-primary-600 text-white"); reach for slotProps when you also need to target the arrow or attach non-class props. |
<Tooltip> is a compound with 2 named slots. Each accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
content | The floating tooltip surface — bg, text, entrance animation. |
arrow | The pointer (rendered unless hideArrow is true). |
<Tooltip
content="Custom-styled"
slotProps={{
content: { className: 'bg-primary-600 text-white font-medium' },
arrow: { className: 'fill-primary-600' },
}}
>
<Button>Trigger</Button>
</Tooltip>document.body via Radix's TooltipPortal. Parent overflow: hidden and transform won't clip it.focus-visible (keyboard focus), not just hover. Icon-only buttons remain discoverable for keyboard-only users.role="tooltip" on the surface and aria-describedby on the trigger. The trigger still needs its own aria-label — screen readers use aria-label (or the trigger's text) as the name and aria-describedby (the tooltip) as the description.prefers-reduced-motion and collapses to instant appear/disappear when the user opts out.<Popover>.<Tooltip> wraps its own <RadixTooltip.Provider> internally, so you don't need to mount a top-level provider. If you want app-wide shared delay + skip config, wrap the app in your own <RadixTooltip.Provider delayDuration={…}> (advanced pattern — most consumers don't need it).