Tooltip displays a floating label on hover or focus to provide brief
contextual information. It wraps the Spar headless Tooltip primitive and adds
Takeoff visual vocabulary — variant, header, description, and arrow slots.
Usage
import { Tooltip } from '@takeoff-ui/react-spar';
<Tooltip>
<Tooltip.Trigger />
<Tooltip.Content>
<Tooltip.Header />
<Tooltip.Description />
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip>
To share delay configuration across a group of tooltips, wrap them in
Tooltip.Provider:
<Tooltip.Provider delayDuration={300} skipDelayDuration={150}>
{}
</Tooltip.Provider>
Playground
function PlaygroundDemo() {
return (
<Tooltip>
<Tooltip.Trigger as={Button}>Hover me</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Simple tooltip</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
);
}
render(<PlaygroundDemo />);
Variants
function VariantsDemo() {
return (
<div className="flex flex-wrap justify-center gap-3">
<Tooltip>
<Tooltip.Trigger as={Button}>White</Tooltip.Trigger>
<Tooltip.Content variant="white">
<Tooltip.Description>White variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Dark</Tooltip.Trigger>
<Tooltip.Content variant="dark">
<Tooltip.Description>Dark variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Info</Tooltip.Trigger>
<Tooltip.Content variant="info">
<Tooltip.Description>Info variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Success</Tooltip.Trigger>
<Tooltip.Content variant="success">
<Tooltip.Description>Success variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Warning</Tooltip.Trigger>
<Tooltip.Content variant="warning">
<Tooltip.Description>Warning variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Danger</Tooltip.Trigger>
<Tooltip.Content variant="danger">
<Tooltip.Description>Danger variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Neutral</Tooltip.Trigger>
<Tooltip.Content variant="neutral">
<Tooltip.Description>Neutral variant</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
</div>
);
}
render(<VariantsDemo />);
Placement
function PlacementDemo() {
return (
<div className="flex flex-wrap justify-center gap-3">
<Tooltip>
<Tooltip.Trigger as={Button}>Top</Tooltip.Trigger>
<Tooltip.Content side="top">
<Tooltip.Description>Positioned top</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Bottom</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Description>Positioned bottom</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Left</Tooltip.Trigger>
<Tooltip.Content side="left">
<Tooltip.Description>Positioned left</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Right</Tooltip.Trigger>
<Tooltip.Content side="right">
<Tooltip.Description>Positioned right</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
</div>
);
}
render(<PlacementDemo />);
function HeaderDemo() {
return (
<Tooltip>
<Tooltip.Trigger as={Button}>With header</Tooltip.Trigger>
<Tooltip.Content variant="info">
<Tooltip.Header>Important</Tooltip.Header>
<Tooltip.Description>This tooltip has a header and description.</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
);
}
render(<HeaderDemo />);
Arrow
Add Tooltip.Arrow as the last child of Tooltip.Content to point at the
trigger. The arrow is bordered by default — its two outer edges continue the
content's outline in the variant's border color, and the neck stays open where
it joins the bubble. This is matched automatically for every variant and
placement.
function ArrowDemo() {
return (
<Tooltip>
<Tooltip.Trigger as={Button}>With arrow</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Tooltip with arrow</Tooltip.Description>
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip>
);
}
render(<ArrowDemo />);
Pass your own children to Tooltip.Arrow to replace the default shape (and
its border). The default draws its border with the .tk-arrow-border layer and
its fill with .tk-arrow-fill — target those classes to restyle it.
Provider
Wrap a group of tooltips in Tooltip.Provider to share delayDuration and a
skipDelayDuration window — once one tooltip in the group has opened, moving to
a sibling within the skip window opens it instantly. Recommended for
toolbar-style clusters (WCAG 1.4.13).
function ProviderDemo() {
return (
<Tooltip.Provider delayDuration={300} skipDelayDuration={150}>
<div className="flex flex-wrap justify-center gap-3">
<Tooltip>
<Tooltip.Trigger as={Button}>First</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Hover, then move to the next one</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Second</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Opens instantly within skip window</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Tooltip>
<Tooltip.Trigger as={Button}>Third</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Same skip-delay behavior</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
</div>
</Tooltip.Provider>
);
}
render(<ProviderDemo />);
Controlled
function ControlledDemo() {
const [open, setOpen] = React.useState(false);
return (
<div className="flex flex-wrap justify-center gap-3">
<Tooltip open={open} onOpenChange={setOpen}>
<Tooltip.Trigger as={Button}>Controlled</Tooltip.Trigger>
<Tooltip.Content>
<Tooltip.Description>Controlled tooltip</Tooltip.Description>
</Tooltip.Content>
</Tooltip>
<Button onClick={() => setOpen(!open)}>{open ? 'Hide' : 'Show'}</Button>
</div>
);
}
render(<ControlledDemo />);
Accessibility & Keyboard
- Trigger uses
aria-describedby pointing to the content element.
- Content has
role="tooltip".
- Tooltip respects
WCAG 1.4.13 — hoverable content remains visible while the
pointer is over it.
- Escape key dismisses the tooltip and returns focus to the trigger.
| Key | Behavior |
|---|
| Escape | Dismiss the tooltip. |
| Tab | Focus trigger shows tooltip; blur hides it. |
API Reference
See
Spar Tooltip docs
for primitive behavior.
| Name | Type | Default | Description |
|---|
| children | React.ReactNode | - | Tooltip trigger and content components |
| id | string | - | Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-trigger and ${id}-content. |
| disabled | boolean | false | Whether tooltip is disabled |
| open | boolean | - | Controlled state for tooltip visibility |
| defaultOpen | boolean | false | Default open state for uncontrolled tooltip |
| delay | number | - | Override provider delay for this tooltip |
| hideDelay | number | 0 | Override provider hide delay for this tooltip |
| Name | Type | Default | Description |
|---|
| onOpenChange | (open: boolean) => void | - | Callback when tooltip open state changes |
Tooltip.Content
Props
| Name | Type | Default | Description |
|---|
| children | React.ReactNode | - | |
| variant | TooltipVariant | 'white' | Color variant. |
| classNames | Partial<Record<"root", string>> | - | Per-slot extra classes. |
| slotProps | Partial<Record<"root", React.HTMLAttributes<HTMLElement>>> | - | Per-slot HTML-attribute overrides. |
| container | HTMLElement | null | document.body | Portal container element. Content is portaled to document.body by default. |
| side | Side | 'top' | Preferred placement relative to trigger |
| align | Align | 'center' | Alignment relative to trigger |
| className | string | - | |
Events
| Name | Type | Default | Description |
|---|
| onOpenAutoFocus | (event: Event) => void | - | Called when auto-focusing on open |
| onCloseAutoFocus | (event: Event) => void | - | Called when auto-focusing on close |
| onEscapeKeyDown | (event: KeyboardEvent) => void | - | Escape key handler |
| onPointerDownOutside | (event: PointerEvent) => void | - | Outside pointer down handler |
Data attributes
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-variant | Always | Reflects the resolved variant for theme recipe scoping. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for the header. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for the description. |
| Name | Definition |
|---|
| TooltipVariant | 'white' | 'dark' | 'info' | 'success' | 'warning' | 'danger' | 'neutral' |
| Side | 'top' | 'right' | 'bottom' | 'left' |
| Align | 'start' | 'center' | 'end' |
| TooltipTriggerRenderProps | { isOpen: boolean; disabled: boolean; show: () => void; hide: () => void } |