# Tooltip (/docs/tooltip)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/tooltip" />

## Basic [#basic]

Use a tooltip for short visual labels on controls. Always give the trigger its own accessible name:
tooltips are not a replacement for `aria-label` or visible text.

<Preview cssProperties="tooltipPlaygroundCssProperties">
  <TooltipExample />

  <Preview.Code>
    {`
          import {
            BellIcon,
            Button,
            Tooltip,
            TooltipContent,
            TooltipTrigger,
          } from "moduix";
          import styles from "./tooltip-demo.module.css";

          export function TooltipDemo() {
            return (
              <Tooltip>
                <TooltipTrigger render={<Button />} aria-label="Notifications">
                  <span className={styles.triggerContent}>
                    <BellIcon className={styles.icon} />
                    Notifications
                  </span>
                </TooltipTrigger>
                <TooltipContent>Notifications</TooltipContent>
              </Tooltip>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .triggerContent {
            display: inline-flex;
            align-items: center;
            gap: var(--spacing-2);
          }

          .icon {
            width: 1rem;
            height: 1rem;
          }
        `}
  </Preview.CSS>

  <Preview.CSSProperties>
    {(context) => <TooltipCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <TooltipCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

Tooltip is composed of one trigger and one popup layer managed by `Tooltip`. Keep each
`TooltipTrigger` connected to its matching `Tooltip` root so focus, hover, and delay behavior stay
predictable.

```text
Tooltip
├─ TooltipTrigger
└─ TooltipContent
   └─ portal
      └─ positioner
         └─ popup
            ├─ arrow
            └─ viewport
               └─ content
```

```tsx
<Tooltip>
  <TooltipTrigger aria-label="Notifications">Hover or focus</TooltipTrigger>
  <TooltipContent>Notifications</TooltipContent>
</Tooltip>
```

| Part             | Role                                                                                                           |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| `Tooltip`        | Root state and timing behavior. Handles open state, delay, controlled props, and optional `handle` linking.    |
| `TooltipTrigger` | Interactive anchor element. Opens the tooltip on hover/focus and should keep its own accessible name.          |
| `TooltipContent` | Composed popup layer. Renders `portal`, `positioner`, `popup`, optional `arrow`, and `viewport` automatically. |

In most cases, keep the default composition and style only visible parts with `className`.
Use `classNames`, `container`, and the focused placement props on `TooltipContent` for common
customization. Reach for the slot prop escape hatches only when you need to pass non-class props to
the internal service slots.

## Composition [#composition]

Use `Tooltip` for root state and behavior props such as `open`, `defaultOpen`,
`onOpenChange`, and `handle`.

`className` styles the visible popup. `classNames` styles the internal service slots that are
hidden from the default composition. Use `container`, `withArrow={false}` to hide the arrow,
`arrow` to replace its graphic, and placement props such as `side` or `sideOffset` for positioning.
Use `slotProps` only when you need to pass non-class props to the internal Base UI service slots:

```tsx
<TooltipContent
  className={styles.popup}
  classNames={{
    portal: styles.portal,
    positioner: styles.positioner,
    arrow: styles.arrow,
    viewport: styles.viewport,
  }}
  slotProps={{
    portal: { keepMounted: true },
    positioner: { collisionPadding: 8 },
    arrow: { style: { transformOrigin: 'center' } },
  }}
/>
```

## Examples [#examples]

### Toolbar [#toolbar]

Wrap related tooltips in one `TooltipProvider` to share delay behavior across a toolbar.

<Preview>
  <ToolbarTooltipExample />

  <Preview.Code>
    {`
          import {
            InfoIcon,
            PlusIcon,
            ShareIcon,
            Tooltip,
            TooltipContent,
            TooltipProvider,
            TooltipTrigger,
          } from "moduix";
          import styles from "./tooltip-demo.module.css";

          export function ToolbarTooltipDemo() {
            return (
              <TooltipProvider delay={300}>
                <div className={styles.toolbar}>
                  <Tooltip>
                    <TooltipTrigger aria-label="Add item" data-variant="ghost">
                      <PlusIcon className={styles.icon} />
                    </TooltipTrigger>
                    <TooltipContent sideOffset={16}>Add item</TooltipContent>
                  </Tooltip>

                  <Tooltip>
                    <TooltipTrigger aria-label="Share" data-variant="ghost">
                      <ShareIcon className={styles.icon} />
                    </TooltipTrigger>
                    <TooltipContent sideOffset={16}>Share</TooltipContent>
                  </Tooltip>

                  <Tooltip>
                    <TooltipTrigger aria-label="Details" data-variant="ghost">
                      <InfoIcon className={styles.icon} />
                    </TooltipTrigger>
                    <TooltipContent sideOffset={16}>Details</TooltipContent>
                  </Tooltip>
                </div>
              </TooltipProvider>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .icon {
            width: 1rem;
            height: 1rem;
          }

          .toolbar {
            display: inline-flex;
            align-items: center;
            gap: var(--border-width-sm);
            padding: var(--spacing-1);
            border: var(--border-width-sm) solid var(--color-border);
            border-radius: var(--radius-lg);
            background: var(--color-muted);
          }
        `}
  </Preview.CSS>
</Preview>

### Without Arrow [#without-arrow]

Set `arrow` to `false` when the popup should read as a compact floating label.

<Preview>
  <TooltipWithoutArrowExample />

  <Preview.Code>
    {`
          import {
            Button,
            Tooltip,
            TooltipContent,
            TooltipTrigger,
          } from "moduix";

          export function TooltipWithoutArrowDemo() {
            return (
              <Tooltip>
                <TooltipTrigger render={<Button />} aria-label="Tooltip without arrow">
                  Hover or focus
                </TooltipTrigger>
                <TooltipContent withArrow={false}>Tooltip without arrow</TooltipContent>
              </Tooltip>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Side Control [#side-control]

Pass placement props directly to `TooltipContent` for the common positioning cases.

<Preview>
  <SideControlTooltipExample />

  <Preview.Code>
    {`
          import {
            Button,
            Tooltip,
            TooltipContent,
            TooltipTrigger,
          } from "moduix";
          import { useState } from "react";
          import styles from "./tooltip-demo.module.css";

          type TooltipSide = "top" | "right" | "bottom" | "left";
          export function SideControlTooltip() {
            const [side, setSide] = useState("top" as TooltipSide);

            return (
              <div className={styles.stack}>
                <div className={styles.sideButtons}>
                  {tooltipSides.map((item) => (
                    <button
                      key={item}
                      type="button"
                      className={styles.sideButton}
                      data-active={item === side || undefined}
                      onClick={() => setSide(item)}
                    >
                      {item}
                    </button>
                  ))}
                </div>

                <Tooltip>
                  <TooltipTrigger render={<Button />} aria-label={\`Tooltip side: \${side}\`}>
                    Hover or focus
                  </TooltipTrigger>
                  <TooltipContent side={side}>Side: {side}</TooltipContent>
                </Tooltip>
              </div>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .stack {
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: var(--spacing-3);
          }

          .sideButtons {
            display: inline-flex;
            align-items: center;
            flex-wrap: wrap;
            gap: var(--spacing-1);
            padding: var(--spacing-1);
            border: var(--border-width-sm) solid var(--color-border);
            border-radius: var(--radius-md);
            background: var(--color-muted);
          }

          .sideButton {
            min-width: 4.75rem;
            min-height: 2rem;
            padding: 0 var(--spacing-3);
            border: var(--border-width-sm) solid var(--color-border);
            border-radius: var(--radius-sm);
            background: var(--color-background);
            color: var(--color-foreground);
            font: inherit;
            font-size: var(--text-sm);
            line-height: var(--line-height-text-sm);
            font-weight: var(--weight-medium);
            text-transform: capitalize;
            cursor: pointer;
            transition:
              background-color var(--transition-default),
              color var(--transition-default);

            &[data-active] {
              background: var(--color-primary);
              color: var(--color-primary-foreground);
            }
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          const tooltipSides = ["top", "right", "bottom", "left"] as TooltipSide[];
        `}
  </Preview.Data>
</Preview>

### Detached Trigger [#detached-trigger]

Use `createTooltipHandle` when the trigger and tooltip content cannot live in the same tree.

<Preview>
  <DetachedTriggerTooltipExample />

  <Preview.Code>
    {`
          import {
            Button,
            Tooltip,
            TooltipContent,
            TooltipTrigger,
            createTooltipHandle,
          } from "moduix";
          import { useMemo } from "react";

          export function DetachedTriggerTooltip() {
            const tooltipHandle = useMemo(() => createTooltipHandle(), []);

            return (
              <>
                <TooltipTrigger
                  handle={tooltipHandle}
                  render={<Button />}
                  aria-label="Detached tooltip"
                >
                  Detached trigger
                </TooltipTrigger>
                <Tooltip handle={tooltipHandle}>
                  <TooltipContent>Linked with handle.</TooltipContent>
                </Tooltip>
              </>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Multiple Triggers [#multiple-triggers]

Share one tooltip between several triggers by linking them with the same handle and rendering the
active trigger payload.

<Preview>
  <MultipleTriggersTooltipExample />

  <Preview.Code>
    {`
          import {
            InfoIcon,
            PlusIcon,
            ShareIcon,
            Tooltip,
            TooltipContent,
            TooltipProvider,
            TooltipTrigger,
            createTooltipHandle,
          } from "moduix";
          import { useMemo } from "react";
          import styles from "./tooltip-demo.module.css";

          export function MultipleTriggersTooltip() {
            const tooltipHandle = useMemo(
              () => createTooltipHandle<{ text: string }>(),
              [],
            );

            return (
              <TooltipProvider delay={250}>
                <div className={styles.row}>
                  <TooltipTrigger
                    aria-label="Create"
                    handle={tooltipHandle}
                    payload={{ text: "Create" }}
                    data-variant="ghost"
                  >
                    <PlusIcon className={styles.icon} />
                  </TooltipTrigger>
                  <TooltipTrigger
                    aria-label="Share"
                    handle={tooltipHandle}
                    payload={{ text: "Share" }}
                    data-variant="ghost"
                  >
                    <ShareIcon className={styles.icon} />
                  </TooltipTrigger>
                  <TooltipTrigger
                    aria-label="Details"
                    handle={tooltipHandle}
                    payload={{ text: "Details" }}
                    data-variant="ghost"
                  >
                    <InfoIcon className={styles.icon} />
                  </TooltipTrigger>

                  <Tooltip handle={tooltipHandle}>
                    {({ payload }) => <TooltipContent>{payload?.text}</TooltipContent>}
                  </Tooltip>
                </div>
              </TooltipProvider>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .icon {
            width: 1rem;
            height: 1rem;
          }

          .row {
            display: flex;
            align-items: center;
            justify-content: center;
            flex-wrap: wrap;
            gap: var(--spacing-2);
          }
        `}
  </Preview.CSS>
</Preview>

### Custom Styles [#custom-styles]

Use `className` for the popup itself, `classNames` for internal slots such as the arrow, and
CSS variables for broad theme changes. Portal, positioner, and viewport are rendered automatically.

<Preview>
  <CustomStylesTooltipExample />

  <Preview.Code>
    {`
          import {
            Tooltip,
            TooltipContent,
            TooltipTrigger,
          } from "moduix";
          import styles from "./tooltip-demo.module.css";

          export function CustomStylesTooltip() {
            return (
              <Tooltip>
                <TooltipTrigger
                  aria-label="Custom styled tooltip"
                  className={styles.customTrigger}
                >
                  Custom style
                </TooltipTrigger>
                <TooltipContent
                  className={styles.customPopup}
                  classNames={{
                    portal: styles.customPortal,
                    positioner: styles.customPositioner,
                    arrow: styles.customArrow,
                    viewport: styles.customViewport,
                  }}
                >
                  Styled through className
                </TooltipContent>
              </Tooltip>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .customPopup {
            --tooltip-bg: var(--color-primary);
            --tooltip-color: var(--color-primary-foreground);
            --tooltip-border-color: color-mix(in oklab, var(--color-primary), black 18%);
            --tooltip-arrow-stroke-color: var(--tooltip-border-color);
            --tooltip-padding-x: var(--spacing-3);
            --tooltip-padding-y: var(--spacing-2);
          }

          .customPortal {
            z-index: var(--z-popup);
          }

          .customPositioner {
            filter: drop-shadow(0 0.5rem 1rem rgb(0 0 0 / 0.14));
          }

          .customViewport {
            min-width: 10rem;
            text-align: left;
          }

          .customTrigger {
            --tooltip-trigger-bg: var(--color-primary);
            --tooltip-trigger-bg-hover: color-mix(in oklab, var(--color-primary), white 10%);
            --tooltip-trigger-bg-active: color-mix(in oklab, var(--color-primary), black 12%);
            --tooltip-trigger-border-color: color-mix(in oklab, var(--color-primary), black 18%);
            --tooltip-trigger-color: var(--color-primary-foreground);
          }

          .customArrow {
            --tooltip-arrow-width: 1.5rem;
            --tooltip-arrow-height: 0.75rem;
          }
        `}
  </Preview.CSS>
</Preview>
