# Tooltip

A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.

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

export default function TooltipDemo() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="outline" />}>Hover</TooltipTrigger>
      <TooltipContent>Add to library</TooltipContent>
    </Tooltip>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. `states.ts` provides the positioner's layer, and
   `state-styles.ts` is how the popup and its arrow read their Base UI state.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/tooltip.tsx"
   "use client"

   import type { CSSProperties } from "react"
   import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"

   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import { color, duration, ease, layer, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     hidden: { opacity: 0 },
     content: {
       zIndex: layer.popup,
       display: "inline-flex",
       alignItems: "center",
       gap: space.s1_5,
       width: "fit-content",
       maxWidth: "20rem",
       borderRadius: radius.md,
       paddingInline: space.s3,
       paddingBlock: space.s1_5,
       backgroundColor: color.foreground,
       color: color.background,
       boxShadow: `inset 0 1px 0 ${color.buttonPrimaryHighlight}, 0 4px 12px -2px ${color.popupShadowFar}`,
       fontSize: text.xs,
       lineHeight: leading.xs,
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: duration.fade,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
     instant: { transitionDuration: "0s" },
     arrow: {
       zIndex: layer.popup,
       width: space.s2_5,
       height: space.s2_5,
       borderRadius: 2,
       backgroundColor: color.foreground,
     },
   })

   const arrowSides = stylex.create({
     top: {
       bottom: `calc(-1 * ${space.s2_5})`,
       transform: "translateY(calc(-50% - 2px)) rotate(45deg)",
     },
     bottom: { top: space.s1, transform: "translateY(calc(-50% - 2px)) rotate(45deg)" },
     left: { right: `calc(-1 * ${space.s1})`, transform: "rotate(45deg)" },
     right: { left: `calc(-1 * ${space.s1})`, transform: "rotate(45deg)" },
     "inline-start": { right: `calc(-1 * ${space.s1})`, transform: "rotate(45deg)" },
     "inline-end": { left: `calc(-1 * ${space.s1})`, transform: "rotate(45deg)" },
   })

   const slide = stylex.create({
     top: { transform: `scale(0.95) translateY(${space.s2})` },
     bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
     left: { transform: `scale(0.95) translateX(${space.s2})` },
     right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
     "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
     "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type StateOverrides = Overrides & { style?: CSSProperties }

   function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
     return <TooltipPrimitive.Provider delay={delay} {...props} />
   }

   function Tooltip<Payload>(props: TooltipPrimitive.Root.Props<Payload>) {
     return <TooltipPrimitive.Root {...props} />
   }

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

   type TooltipContentProps = Omit<TooltipPrimitive.Popup.Props, "className" | "style"> &
     Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
     StateOverrides

   function TooltipContent({
     xstyle,
     style,
     className,
     side = "top",
     sideOffset = 4,
     align = "center",
     alignOffset = 0,
     children,
     ...props
   }: TooltipContentProps) {
     return (
       <TooltipPrimitive.Portal>
         <TooltipPrimitive.Positioner
           align={align}
           alignOffset={alignOffset}
           side={side}
           sideOffset={sideOffset}
           {...stylex.props(states.positioner)}
         >
           <TooltipPrimitive.Popup
             data-slot="tooltip-content"
             {...props}
             {...stateStyles(
               (state) => [
                 styles.content,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   styles.hidden,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   slide[state.side],
                 state.instant && styles.instant,
                 xstyle,
               ],
               {
                 className,
                 style,
               },
             )}
           >
             {children}
             <TooltipPrimitive.Arrow
               data-slot="tooltip-arrow"
               {...stateStyles((state) => [styles.arrow, arrowSides[state.side]])}
             />
           </TooltipPrimitive.Popup>
         </TooltipPrimitive.Positioner>
       </TooltipPrimitive.Portal>
     )
   }

   export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
   ```

4. Update the import paths to match your project setup.

5. Wrap your app in the `TooltipProvider`.

   ```tsx title="main.tsx"
   import "./styles/theme.css"
   import "./styles/interop.css"
   import { StrictMode } from "react"
   import { createRoot } from "react-dom/client"
   import { TooltipProvider } from "@/components/ui/tooltip"
   import App from "./App"

   createRoot(document.getElementById("root")!).render(
     <StrictMode>
       <TooltipProvider>
         <App />
       </TooltipProvider>
     </StrictMode>,
   )
   ```

## Usage

```tsx
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
```

```tsx
<Tooltip>
  <TooltipTrigger>Hover</TooltipTrigger>
  <TooltipContent>Add to library</TooltipContent>
</Tooltip>
```

## Composition

```text
TooltipProvider           once, at the app root
└── Tooltip
    ├── TooltipTrigger
    └── TooltipContent    portal + positioner + popup + arrow
```

`TooltipTrigger` renders a `<button>`. Pass `render` to use a different element
as the trigger, such as a `Button` variant.

Every tooltip inside the provider shares its `delay`. While one is open, moving
to a neighbour opens the next one with no delay and no transition.

> **Warning**
>
> A tooltip opens on hover and on keyboard focus, never on press. A touch screen
> has no hover, so the tooltip never opens there. Put nothing in it that a reader
> cannot also reach another way, and give an icon-only trigger an `aria-label` of
> its own rather than relying on the tooltip's text.

## Side

Use `side` on `TooltipContent` to pick the edge it opens from. Base UI flips the
side when there is no room, and the arrow follows.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { HStack } from "@/components/ui/stack"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

const styles = stylex.create({
  trigger: { textTransform: "capitalize" },
})

const sides = ["left", "top", "bottom", "right"] as const

export default function TooltipSides() {
  return (
    <HStack gap={2} wrap="wrap">
      {sides.map((side) => (
        <Tooltip key={side}>
          <TooltipTrigger render={<Button variant="outline" xstyle={styles.trigger} />}>
            {side}
          </TooltipTrigger>
          <TooltipContent side={side}>Add to library</TooltipContent>
        </Tooltip>
      ))}
    </HStack>
  )
}
```

## Disabled button

A disabled button fires no pointer events, so wrap it in a `<span>` and make
the span the trigger.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

const styles = stylex.create({
  wrapper: { display: "inline-block", width: "fit-content" },
})

export default function TooltipDisabled() {
  return (
    <Tooltip>
      <TooltipTrigger render={<span {...stylex.props(styles.wrapper)} />}>
        <Button variant="outline" disabled>
          Disabled
        </Button>
      </TooltipTrigger>
      <TooltipContent>This feature is currently unavailable</TooltipContent>
    </Tooltip>
  )
}
```

### Keyboard shortcut

Put a [`Kbd`](https://carbon.style/docs/components/kbd) in the content to show the shortcut beside
the label.

```tsx
import { Button } from "@/components/ui/button"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { HStack } from "@/components/ui/stack"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"

export default function TooltipKbd() {
  return (
    <Tooltip>
      <TooltipTrigger render={<Button variant="outline" />}>Save</TooltipTrigger>
      <TooltipContent>
        <HStack gap={2} align="center">
          Save changes
          <KbdGroup>
            <Kbd>⌘</Kbd>
            <Kbd>S</Kbd>
          </KbdGroup>
        </HStack>
      </TooltipContent>
    </Tooltip>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Tooltip` part of the same name and accepts its
props. See the [Base UI reference](https://base-ui.com/react/components/tooltip)
for the full list. `TooltipContent` also takes `xstyle` and `className`, and
`style` for real inline CSS, merged over whatever its Base UI state resolves
to.

### TooltipProvider

| Prop         | Type     | Default |
| ------------ | -------- | ------- |
| `delay`      | `number` | `0`     |
| `closeDelay` | `number` | —       |
| `timeout`    | `number` | `400`   |

`timeout` is how long after one tooltip closes the next one still opens
instantly.

### Tooltip

| Prop                    | Type                               | Default  |
| ----------------------- | ---------------------------------- | -------- |
| `open`                  | `boolean`                          | —        |
| `defaultOpen`           | `boolean`                          | `false`  |
| `onOpenChange`          | `(open: boolean, details) => void` | —        |
| `disabled`              | `boolean`                          | `false`  |
| `disableHoverablePopup` | `boolean`                          | `false`  |
| `trackCursorAxis`       | `"none" \| "x" \| "y" \| "both"`   | `"none"` |
| `handle`                | `TooltipHandle`                    | —        |

### TooltipTrigger

| Prop           | Type      | Default |
| -------------- | --------- | ------- |
| `delay`        | `number`  | `600`   |
| `closeDelay`   | `number`  | `0`     |
| `closeOnClick` | `boolean` | `true`  |
| `disabled`     | `boolean` | `false` |
| `payload`      | `Payload` | —       |

A `delay` on the trigger is only read when the provider sets none.

### TooltipContent

| Prop          | Type                                     | Default    |
| ------------- | ---------------------------------------- | ---------- |
| `side`        | `"top" \| "bottom" \| "left" \| "right"` | `"top"`    |
| `sideOffset`  | `number`                                 | `4`        |
| `align`       | `"start" \| "center" \| "end"`           | `"center"` |
| `alignOffset` | `number`                                 | `0`        |
| `style`       | `CSSProperties`                          | —          |

The popup is as wide as its text and wraps past `20rem`. Change the width
through `xstyle`.

### Data attributes

| Attribute                                  | On          | Effect                                                       |
| ------------------------------------------ | ----------- | ------------------------------------------------------------ |
| `data-slot="tooltip-content"`              | the popup   | Popup surface                                                |
| `data-slot="tooltip-arrow"`                | the arrow   | Positioned by Base UI on the cross axis, by the side on ours |
| `data-side`, `data-align`                  | both        | Which way the popup slides in from, and where the arrow sits |
| `data-instant`                             | the popup   | Base UI writes it when the transition is skipped             |
| `data-starting-style`, `data-ending-style` | the popup   | Base UI writes them while the popup animates                 |
| `data-popup-open`                          | the trigger | Base UI writes it while this trigger's tooltip is open       |
