# Tooltip

> Show a brief, nonessential hint when a control receives hover or keyboard focus.

Source: https://www.honestui.com/docs/components/tooltip

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Tooltip,
  TooltipPopup,
  TooltipTrigger,
} from "@/components/honest-ui/ui/tooltip"

export function TooltipDemo() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="secondary" />}>
        Hover me
      </TooltipTrigger>
      <TooltipPopup>Helpful hint</TooltipPopup>
    </Tooltip>
  )
}

```

## Overview [#overview]

Use a Tooltip to provide a short hint for an element: what an icon-only button does, the full text behind a truncated label, a keyboard shortcut for a familiar action. The word "nonessential" matters — anything required to complete a task must be visible without hovering or focusing, because touch users cannot hover at all and many people never dwell long enough to trigger the tooltip.

Tooltips are display-only. They close when focus moves on, so they cannot hold buttons, links, or form fields; use a Popover for interactive content opened deliberately.

## Anatomy [#anatomy]

A tooltip has a trigger and popup content, plus an optional arrow rendered by default. Wrap several tooltips in `TooltipProvider` to share delay behavior across them. Content should be short — one phrase or sentence — because it appears in small type next to the cursor and disappears as soon as attention moves.

## Behavior [#behavior]

**Showing.** Tooltips appear on hover after a rest delay (600 ms by default) and on keyboard focus with no delay, so keyboard users get hints immediately upon tabbing. Moving between grouped tooltips inside one `TooltipProvider` skips the wait: once one is visible, adjacent tooltips open instantly, and a closed tooltip reopens instantly if another opens within 400 ms.

**Dismissing.** Hovering away or blurring the trigger closes the tooltip. Pressing <kbd>Escape</kbd> dismisses it too, which matters when a tooltip covers something the person needs to see or click. A tooltip never takes focus itself — it stays purely visual.

**Placement.** `side` (`top` by default), `align`, and `sideOffset` sit on `TooltipPopup`. Set `showArrow={false}` to remove the arrow. The popup flips and shifts to stay in the viewport.

## Accessibility [#accessibility]

The trigger keeps its own accessible name and state; the popup gets no ARIA role and no automatic description link. That is intentional — Base UI treats tooltip text as supplementary, because screen readers do not reliably announce hover content. The practical consequence: every trigger must make sense without its tooltip. Icon-only controls need an explicit `aria-label`, and the tooltip becomes redundant reinforcement rather than the only source of the name.

Keyboard parity is built in: focusing the trigger shows the same tooltip a mouse user sees, and <kbd>Escape</kbd> dismisses without moving focus. Because the tooltip does not receive focus, nothing about it appears in the tab order.

Text wraps inside a capped width instead of forming a single long line, so hints remain readable near screen edges. Colors come from theme tokens for dark mode, entrance motion respects reduced-motion preferences through the component's transition classes, and placement uses logical sides that respect document direction.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;tooltip&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/tooltip.tsx

```tsx
"use client"

import { Tooltip as TooltipPrimitive } from "@base-ui-components/react/tooltip"

import { cn } from "@/lib/utils"

const TooltipProvider = TooltipPrimitive.Provider

const Tooltip = TooltipPrimitive.Root

function TooltipTrigger(props: TooltipPrimitive.Trigger.Props) {
  return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}

function TooltipPopup({
  className,
  arrowClassName,
  align = "center",
  sideOffset = 4,
  side = "top",
  showArrow = true,
  children,
  ...props
}: TooltipPrimitive.Popup.Props & {
  align?: TooltipPrimitive.Positioner.Props["align"]
  side?: TooltipPrimitive.Positioner.Props["side"]
  sideOffset?: TooltipPrimitive.Positioner.Props["sideOffset"]
  arrowClassName?: string
  showArrow?: boolean
}) {
  return (
    <TooltipPrimitive.Portal>
      <TooltipPrimitive.Positioner
        data-slot="tooltip-positioner"
        className="z-[var(--hui-z-index-portal)]"
        sideOffset={sideOffset}
        align={align}
        side={side}
      >
        <TooltipPrimitive.Popup
          data-slot="tooltip-content"
          className={cn(
            "relative box-border w-fit max-w-[400px] rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] px-[var(--hui-space-3)] py-[var(--hui-space-2)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-soft)] [font-size:var(--hui-font-size-mini)] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-mini)] [line-height:var(--hui-line-height-mini)] [transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out)] data-ending-style:opacity-0 data-instant:[transition-duration:0ms] data-starting-style:opacity-0 motion-safe:[transition:opacity_var(--hui-duration-fast)_var(--hui-ease-out),transform_var(--hui-duration-fast)_var(--hui-ease-out)] motion-safe:[&[data-side=top][data-starting-style]]:[transform:translateY(calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=top][data-ending-style]]:[transform:translateY(calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=right][data-starting-style]]:[transform:translateX(var(--hui-space-1))] motion-safe:[&[data-side=right][data-ending-style]]:[transform:translateX(var(--hui-space-1))] motion-safe:[&[data-side=bottom][data-starting-style]]:[transform:translateY(var(--hui-space-1))] motion-safe:[&[data-side=bottom][data-ending-style]]:[transform:translateY(var(--hui-space-1))] motion-safe:[&[data-side=left][data-starting-style]]:[transform:translateX(calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=left][data-ending-style]]:[transform:translateX(calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=top][data-align=start][data-starting-style]]:[transform:translate(calc(-1*var(--hui-space-1)),calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=top][data-align=start][data-ending-style]]:[transform:translate(calc(-1*var(--hui-space-1)),calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=top][data-align=end][data-starting-style]]:[transform:translate(var(--hui-space-1),calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=top][data-align=end][data-ending-style]]:[transform:translate(var(--hui-space-1),calc(-1*var(--hui-space-1)))] motion-safe:[&[data-side=bottom][data-align=start][data-starting-style]]:[transform:translate(calc(-1*var(--hui-space-1)),var(--hui-space-1))] motion-safe:[&[data-side=bottom][data-align=start][data-ending-style]]:[transform:translate(calc(-1*var(--hui-space-1)),var(--hui-space-1))] motion-safe:[&[data-side=bottom][data-align=end][data-starting-style]]:[transform:translate(var(--hui-space-1),var(--hui-space-1))] motion-safe:[&[data-side=bottom][data-align=end][data-ending-style]]:[transform:translate(var(--hui-space-1),var(--hui-space-1))]",
            className
          )}
          {...props}
        >
          {children}
        </TooltipPrimitive.Popup>
        {showArrow && <TooltipArrow className={arrowClassName} />}
      </TooltipPrimitive.Positioner>
    </TooltipPrimitive.Portal>
  )
}

function TooltipArrow({
  className,
  children,
  ...props
}: TooltipPrimitive.Arrow.Props) {
  return (
    <TooltipPrimitive.Arrow
      data-slot="tooltip-arrow"
      className={cn(
        "z-[var(--hui-z-index-portal)] [filter:drop-shadow(0_1px_0_var(--hui-color-border-base-primary))_drop-shadow(0_1px_1px_var(--hui-color-border-base-primary))] data-[side=top]:bottom-[-7px] data-[side=bottom]:top-0 data-[side=bottom]:-translate-y-full data-[side=bottom]:rotate-180 data-[side=left]:right-0 data-[side=left]:translate-x-full data-[side=left]:-translate-y-1/2 data-[side=left]:-rotate-90 data-[side=inline-start]:right-0 data-[side=inline-start]:translate-x-full data-[side=inline-start]:-translate-y-1/2 data-[side=inline-start]:-rotate-90 data-[side=right]:left-0 data-[side=right]:-translate-x-full data-[side=right]:-translate-y-1/2 data-[side=right]:rotate-90 data-[side=inline-end]:left-0 data-[side=inline-end]:-translate-x-full data-[side=inline-end]:-translate-y-1/2 data-[side=inline-end]:rotate-90 [&_svg]:text-[var(--hui-color-background-base-primary)]",
        className
      )}
      {...props}
    >
      {children ?? (
        <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
          <path d="M0 0H20L10 10L0 0Z" fill="currentColor" />
        </svg>
      )}
    </TooltipPrimitive.Arrow>
  )
}

export {
  TooltipProvider,
  Tooltip,
  TooltipTrigger,
  TooltipArrow,
  TooltipPopup,
  TooltipPopup as TooltipContent,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Tooltip,
  TooltipPopup,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
```

```tsx
<Tooltip>
  <TooltipTrigger render={<Button variant="secondary" />}>
    Hover me
  </TooltipTrigger>
  <TooltipPopup>Helpful hint</TooltipPopup>
</Tooltip>
```

**Grouping Tooltips**

To group multiple tooltips so they appear instantly after the first one is opened, wrap them in `TooltipProvider`. The grouping logic ensures that once a tooltip becomes visible, the adjacent tooltips will be shown instantly.

```tsx
<TooltipProvider>
  <Tooltip>
    <TooltipTrigger render={<Button variant="secondary" />}>
      Tooltip 1
    </TooltipTrigger>
    <TooltipPopup>Content 1</TooltipPopup>
  </Tooltip>
  <Tooltip>
    <TooltipTrigger render={<Button variant="secondary" />}>
      Tooltip 2
    </TooltipTrigger>
    <TooltipPopup>Content 2</TooltipPopup>
  </Tooltip>
</TooltipProvider>
```

## Don't do this [#dont-do-this]

### Nesting the trigger inside a button [#nesting-the-trigger-inside-a-button]

```tsx
// Bad
<Button>
  <TooltipTrigger>Save</TooltipTrigger>
</Button>
```

```tsx
// Good
<Tooltip>
  <TooltipTrigger render={<Button variant="secondary" />}>
    Save
  </TooltipTrigger>
  <TooltipPopup>Save changes to this draft</TooltipPopup>
</Tooltip>
```

`TooltipTrigger` renders a real `<button>` element, so wrapping one button inside another produces invalid HTML that browsers repair unpredictably — splitting event handling and breaking the accessibility tree. Use the `render` prop to merge the trigger onto the Button itself.

### Hiding essential information behind the tooltip [#hiding-essential-information-behind-the-tooltip]

```tsx
// Bad
<Input aria-label="API key" />
<Tooltip>
  <TooltipTrigger aria-label="Help"><InfoIcon /></TooltipTrigger>
  <TooltipPopup>Your API key is shown only once.</TooltipPopup>
</Tooltip>
```

```tsx
// Good
<Input aria-label="API key" />
<p className="text-muted-foreground text-xs">
  Your API key is shown only once.
</p>
```

Touch users can never hover, keyboard users lose the message the moment focus moves on, and screen readers may not announce it at all. If acting incorrectly causes data loss or confusion, the warning belongs in permanent, visible text.

### Putting controls inside a tooltip [#putting-controls-inside-a-tooltip]

```tsx
// Bad
<TooltipPopup>
  <p>Draft saved</p>
  <Button size="sm">Undo</Button>
</TooltipPopup>
```

```tsx
// Good
<Popover>
  <PopoverTrigger render={<Button variant="ghost" />}>
    Draft saved
  </PopoverTrigger>
  <PopoverPopup>
    <PopoverClose render={<Button variant="secondary" />}>Undo</PopoverClose>
  </PopoverPopup>
</Popover>
```

A tooltip closes when focus leaves the trigger, so a control inside it cannot be reached: tabbing toward it dismisses the surface that contains it. Interactive follow-up actions belong in a Popover, which holds focus predictably until dismissed.

## Examples [#examples]

### Icon-only button [#icon-only-button]

The `aria-label` names the control; the tooltip repeats it visually for sighted users.

```tsx
import { Info as InfoIcon } from "honestui/icons"

import { Button } from "@/components/honest-ui/ui/button"
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/honest-ui/ui/tooltip"

export function TooltipIconRow() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="secondary" size="icon" />} aria-label="Info"><InfoIcon /></TooltipTrigger>
      <TooltipPopup>Includes keyboard and screen reader support.</TooltipPopup>
    </Tooltip>
  )
}

```

### Keyboard shortcut hint [#keyboard-shortcut-hint]

Shortcuts are the classic tooltip use: visible context for people who already know the action.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/honest-ui/ui/tooltip"

export function TooltipKeyboardHints() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="secondary" />}>Save</TooltipTrigger>
      <TooltipPopup>Press Ctrl S to save changes.</TooltipPopup>
    </Tooltip>
  )
}

```

### Grouped toolbar [#grouped-toolbar]

Inside one `TooltipProvider`, moving across the toolbar swaps tooltips instantly instead of waiting out the delay each time.

```tsx
import { Bold as BoldIcon, Italic as ItalicIcon, Underline as UnderlineIcon } from "honestui/icons"

import { Toggle, ToggleGroup } from "@/components/honest-ui/ui/toggle"
import {
  Tooltip,
  TooltipPopup,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/honest-ui/ui/tooltip"

export function TooltipDemo() {
  return (
    <TooltipProvider>
      <ToggleGroup defaultValue={["bold"]} multiple>
        <Tooltip>
          <TooltipTrigger
            render={<Toggle value="bold" aria-label="Toggle bold" />}
          >
            <BoldIcon />
          </TooltipTrigger>
          <TooltipPopup>Bold</TooltipPopup>
        </Tooltip>
        <Tooltip>
          <TooltipTrigger
            render={
              <Toggle value="italic" aria-label="Toggle italic" />
            }
          >
            <ItalicIcon />
          </TooltipTrigger>
          <TooltipPopup>Italic</TooltipPopup>
        </Tooltip>
        <Tooltip>
          <TooltipTrigger
            render={
              <Toggle value="underline" aria-label="Toggle underline" />
            }
          >
            <UnderlineIcon />
          </TooltipTrigger>
          <TooltipPopup>Underline</TooltipPopup>
        </Tooltip>
      </ToggleGroup>
    </TooltipProvider>
  )
}

```

## API reference [#api-reference]

All parts forward their matching Base UI Tooltip props. Honest UI adds presentation shortcuts on `TooltipPopup`:

| Prop             | Values                           | Default  |
| ---------------- | -------------------------------- | -------- |
| `side`           | `top`, `right`, `bottom`, `left` | `top`    |
| `align`          | `start`, `center`, `end`         | `center` |
| `sideOffset`     | number                           | `4`      |
| `showArrow`      | boolean                          | `true`   |
| `arrowClassName` | string                           | —        |

`TooltipProvider` accepts shared `delay`, `closeDelay`, and `timeout` (400 ms by default) that govern grouping behavior for every tooltip beneath it. The root supports controlled `open` / `onOpenChange` and a `disabled` prop. `TooltipContent` aliases `TooltipPopup`.

See the [Base UI Tooltip API](https://base-ui.com/react/components/tooltip#api-reference).
