# Tooltip

A short text that shows when the pointer or the focus is on an element.

```tsx
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>
    </>
  )
}
```

## Usage guidelines

- **Use `Hint` first.** `Hint` is a tooltip in one component. Use the `Tooltip` parts for a controlled state or for more props.
- **A few words only.** A tooltip explains an icon-only button or shows the full text of a truncated value.
- **Nothing essential.** A touch screen has no hover. For necessary information or a control, use [Popover](/docs/components/popover) or text on the page.
- **Not the `title` attribute.** Use `Hint` in its place.

<Callout tone="warning" title="A tooltip is not an accessible name">
  A tooltip is a visual aid. An icon-only button must also have an `aria-label`.
</Callout>

## Anatomy

Mount `TooltipProvider` one time, at the root of the app. Each tooltip must have this provider above it.

```tsx title="app.tsx"

  return <TooltipProvider>{/* the app */}</TooltipProvider>
}
```

Then import `Hint`, or the three parts of a tooltip.

```tsx title="Anatomy"

<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. |

## Examples

### Side

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.

```tsx
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>
      ))}
    </>
  )
}
```

### Tooltip parts

`TooltipContent` accepts `side`, `align` and `sideOffset`. Use `asChild` on `TooltipTrigger` with a [Button](/docs/components/button) or a link.

```tsx
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>
  )
}
```

### Open state

A tooltip holds its open state by default. To control the state, pass `open` and `onOpenChange` to `Tooltip`.

```tsx
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>
    </>
  )
}
```

### Disabled button

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}>`.

```tsx
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>
  )
}
```

### Truncated text

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.

```tsx
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>
  )
}
```

### Delay

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.

```tsx
<TooltipProvider delayDuration={0}>
  <Hint label="Bold">
    <Button variant="ghost" size="icon" icon={<Bold />} aria-label="Bold" />
  </Hint>
</TooltipProvider>
```

## Accessibility

- The child of `Hint` must be one element that can get the focus, for example a button or a link.
- A tooltip opens on hover and on keyboard focus. <Kbd>Esc</Kbd> closes it.
- Do not put a link or a button in a tooltip.

## API reference

`Hint` accepts only the props of its table. The other parts also accept the props of their Radix UI primitive.

### Hint

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | Tooltip text; keep it to a few words. |
| `children` (required) | `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. |

### TooltipProvider

In ferry-ui, the default `delayDuration` is 250.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  |  |
| `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 `true`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. |

### Tooltip

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `open` | `boolean` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  |  |
| `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 `true`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. |

### TooltipTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### TooltipContent

In ferry-ui, the default `sideOffset` is 6.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `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 `pointerdown` event happens outside of the `Tooltip`. Can be prevented. |
| `asChild` | `boolean` |  |  |
| `side` | `"top" \| "right" \| "bottom" \| "left"` |  |  |
| `sideOffset` | `number` | `6` |  |
| `align` | `"center" \| "start" \| "end"` |  |  |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"top" \| "right" \| "bottom" \| "left", number>>` |  |  |
| `sticky` | `"partial" \| "always"` |  |  |
| `hideWhenDetached` | `boolean` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |
