Primitives
Tooltip
A short text that shows when the pointer or the focus is on an element.
Primitives
A short text that shows when the pointer or the focus is on an element.
import { Button, Hint } from 'ferry-ui'
import { Download, RefreshCw, Settings } from 'lucide-react'
export default function TooltipHero() {
return (
<>
<Hint label="Refresh">
<Button size="icon" icon={<RefreshCw />} aria-label="Refresh" />
</Hint>
<Hint label="Download CSV">
<Button size="icon" icon={<Download />} aria-label="Download CSV" />
</Hint>
<Hint label="Settings">
<Button size="icon" icon={<Settings />} aria-label="Settings" />
</Hint>
</>
)
}Hint first. Hint is a tooltip in one component. Use the Tooltip parts for a controlled state or for more props.title attribute. Use Hint in its place.Mount TooltipProvider one time, at the root of the app. Each tooltip must have this provider above it.
import { TooltipProvider } from 'ferry-ui'
export function App() {
return <TooltipProvider>{/* the app */}</TooltipProvider>
}Then import Hint, or the three parts of a tooltip.
import { Button, Hint, Tooltip, TooltipContent, TooltipTrigger } from 'ferry-ui'
<Hint label="Refresh">
<Button />
</Hint>
<Tooltip>
<TooltipTrigger />
<TooltipContent />
</Tooltip>| Part | Role |
|---|---|
TooltipProvider | Gives the same delay to all the tooltips below it. |
Hint | A tooltip in one component. Its child is the trigger. |
Tooltip | Holds the open state. |
TooltipTrigger | The element that shows the tooltip on hover and on focus. |
TooltipContent | The text bubble. |
The side prop of Hint sets the side of the trigger where the tooltip opens. The default is top. If the side has no room, the tooltip changes side.
import { Button, Hint } from 'ferry-ui'
const SIDES = ['top', 'right', 'bottom', 'left'] as const
export default function TooltipSide() {
return (
<>
{SIDES.map((side) => (
<Hint key={side} label={`Tooltip on the ${side}`} side={side}>
<Button>{side}</Button>
</Hint>
))}
</>
)
}TooltipContent accepts side, align and sideOffset. Use asChild on TooltipTrigger with a Button or a link.
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ferry-ui'
export default function TooltipParts() {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="tiny">
Updated 2 hours ago
</Button>
</TooltipTrigger>
{/* The parts give access to the props of the content: `side`, `align`, `sideOffset`. */}
<TooltipContent side="bottom" align="start">
March 3, 2026 at 14:05
</TooltipContent>
</Tooltip>
)
}A tooltip holds its open state by default. To control the state, pass open and onOpenChange to Tooltip.
import * as React from 'react'
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ferry-ui'
import { Archive } from 'lucide-react'
export default function TooltipControlled() {
const [open, setOpen] = React.useState(false)
return (
<>
<Tooltip open={open} onOpenChange={setOpen}>
<TooltipTrigger asChild>
<Button size="icon" icon={<Archive />} aria-label="Archive project" />
</TooltipTrigger>
<TooltipContent>Archive project</TooltipContent>
</Tooltip>
<span className="text-[13px] text-foreground-light">The tooltip is {open ? 'open' : 'closed'}.</span>
</>
)
}A disabled button gets no hover and no focus. To explain why the action is not available, wrap the button in a <span tabIndex={0}>.
import { Button, Hint } from 'ferry-ui'
import { Trash2 } from 'lucide-react'
export default function TooltipOnDisabledButton() {
return (
<Hint label="Only an admin of the workspace can delete a project">
{/* A disabled button gets no hover and no focus: the span is the trigger. */}
<span tabIndex={0} className="inline-flex rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring">
<Button variant="destructive" icon={<Trash2 />} disabled>
Delete project
</Button>
</span>
</Hint>
)
}A tooltip can show the full text of a truncated value. Add tabIndex={0} to the element, so that keyboard users also get the tooltip.
import { Hint } from 'ferry-ui'
const INVOICES = [
{ id: 'INV-2041', customer: 'Acme International Trading Company', amount: '$4,200.00' },
{ id: 'INV-2042', customer: 'Example Logistics and Supply Partners', amount: '$860.00' },
{ id: 'INV-2043', customer: 'Sample Health and Insurance Services', amount: '$12,940.50' },
]
export default function TooltipOnTruncatedText() {
return (
<ul className="w-80 max-w-full divide-y rounded-lg border bg-surface-100 text-[13px]">
{INVOICES.map((invoice) => (
<li key={invoice.id} className="flex items-center gap-3 px-3 py-2">
<span className="shrink-0 font-mono text-xs text-foreground-lighter">{invoice.id}</span>
<Hint label={invoice.customer}>
{/* `tabIndex={0}` lets keyboard users get the tooltip. */}
<span
tabIndex={0}
className="min-w-0 flex-1 truncate rounded-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{invoice.customer}
</span>
</Hint>
<span className="tabular shrink-0 text-foreground-light">{invoice.amount}</span>
</li>
))}
</ul>
)
}The first tooltip opens after 250 ms. While the pointer moves between triggers, the next tooltips open immediately.
To change the delay for one area, nest a second TooltipProvider. Pass delayDuration in milliseconds.
<TooltipProvider delayDuration={0}>
<Hint label="Bold">
<Button variant="ghost" size="icon" icon={<Bold />} aria-label="Bold" />
</Hint>
</TooltipProvider>Hint must be one element that can get the focus, for example a button or a link.Hint accepts only the props of its table. The other parts also accept the props of their Radix UI primitive.
| Prop | Type | Default |
|---|---|---|
labelRequired | ReactNode | - |
Tooltip text; keep it to a few words. | ||
childrenRequired | ReactNode | - |
The trigger: a single focusable element that forwards its ref. | ||
side | "top" | "right" | "bottom" | "left" | - |
Preferred side of the trigger (default "top"); flips automatically when there is no room. | ||
In ferry-ui, the default delayDuration is 250.
| Prop | Type | Default |
|---|---|---|
childrenRequired | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
delayDuration | number | 250 |
The duration from when the pointer enters the trigger until the tooltip gets opened. | ||
skipDelayDuration | number | 300 |
How much time a user has to enter another trigger without incurring a delay again. | ||
disableHoverableContent | boolean | false |
When | ||
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
open | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
defaultOpen | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onOpenChange | ((open: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
delayDuration | number | 700 |
The duration from when the pointer enters the trigger until the tooltip gets opened. This will override the prop with the same name passed to Provider. | ||
disableHoverableContent | boolean | false |
When | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
In ferry-ui, the default sideOffset is 6.
| Prop | Type | Default |
|---|---|---|
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
aria-label | string | - |
A more descriptive label for accessibility purpose | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | - |
Event handler called when the escape key is down. Can be prevented. | ||
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | - |
Event handler called when the a | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
side | "top" | "right" | "bottom" | "left" | - |
| See the element or the Radix UI primitive this part renders. | ||
sideOffset | number | 6 |
| See the element or the Radix UI primitive this part renders. | ||
align | "center" | "start" | "end" | - |
| See the element or the Radix UI primitive this part renders. | ||
alignOffset | number | - |
| See the element or the Radix UI primitive this part renders. | ||
arrowPadding | number | - |
| See the element or the Radix UI primitive this part renders. | ||
avoidCollisions | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionBoundary | Boundary | Boundary[] | - |
| See the element or the Radix UI primitive this part renders. | ||
collisionPadding | number | Partial<Record<"top" | "right" | "bottom" | "left", number>> | - |
| See the element or the Radix UI primitive this part renders. | ||
sticky | "partial" | "always" | - |
| See the element or the Radix UI primitive this part renders. | ||
hideWhenDetached | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
updatePositionStrategy | "always" | "optimized" | - |
| See the element or the Radix UI primitive this part renders. | ||