# Tooltip

A small popup that describes the element it is attached to, shown on hover or focus.

## Installation

```tsx
import { Tooltip } from "@makeplane/propel/components/tooltip";
```
## Usage

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";

export default function BasicDemo() {
  return (
    <Tooltip label="Create a new project">
      <Button label="New project" variant="secondary" size="sm" stretch="auto" />
    </Tooltip>
  );
}
```
## Examples

### Placement

The `side` prop chooses which edge the popup attaches to. It may flip automatically to stay in view.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";

export default function PlacementDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Tooltip label="Shown above the trigger" side="top">
        <Button label="Top" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
      <Tooltip label="Shown to the right" side="right">
        <Button label="Right" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
      <Tooltip label="Shown below the trigger" side="bottom">
        <Button label="Bottom" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
      <Tooltip label="Shown to the left" side="left">
        <Button label="Left" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
    </div>
  );
}
```

### With arrow

The default tooltip is a flat surface (no caret). Pass `showArrow` when you need a caret pointing back at the trigger.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";

export default function WithArrowDemo() {
  return (
    <Tooltip label="Create a new project" showArrow>
      <Button label="New project" variant="secondary" size="sm" stretch="auto" />
    </Tooltip>
  );
}
```

### With shortcut

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";

export default function WithShortcutDemo() {
  return (
    <Tooltip label="Open command menu" shortcut="⌘ K">
      <Button
        label="Command menu"
        variant="secondary"
        size="sm"
        stretch="auto"
        aria-keyshortcuts="Meta+K"
      />
    </Tooltip>
  );
}
```

### Stacked layout

The default `layout="single"` is a compact chip that wraps long labels within 500px and the available viewport width. Use `layout="stacked"` for short descriptions (Figma multiline); its shortcut sits under the label. Prefer a popover if the copy runs longer than about two lines.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";

export default function StackedDemo() {
  return (
    <Tooltip
      label="A popup that displays information related to an element."
      shortcut="⌘ K"
      layout="stacked"
    >
      <Button
        label="About tooltips"
        variant="secondary"
        size="sm"
        stretch="auto"
        aria-keyshortcuts="Meta+K"
      />
    </Tooltip>
  );
}
```

### Grouped

Wrapping triggers in a `TooltipProvider` shares open/close timing, so moving between adjacent triggers shows the next tooltip immediately instead of restarting the hover delay.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip, TooltipProvider } from "@makeplane/propel/components/tooltip";

export default function GroupedDemo() {
  return (
    <TooltipProvider closeDelay={100}>
      <div className="flex flex-wrap items-center gap-3">
        <Tooltip label="Bold the selection">
          <Button label="Bold" variant="secondary" size="sm" stretch="auto" />
        </Tooltip>
        <Tooltip label="Italicize the selection">
          <Button label="Italic" variant="secondary" size="sm" stretch="auto" />
        </Tooltip>
        <Tooltip label="Underline the selection">
          <Button label="Underline" variant="secondary" size="sm" stretch="auto" />
        </Tooltip>
      </div>
    </TooltipProvider>
  );
}
```

### Controlled

Own open state in React with `open` / `onOpenChange`. The secondary action calls `setOpen(true)`; Escape and blur still flow through `onOpenChange`, so the tooltip cannot stick open. The live “React state” label shows that ownership.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";
import * as React from "react";

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Tooltip
        label="Archiving moves this project out of the active list"
        open={open}
        onOpenChange={setOpen}
      >
        <Button label="Archive" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
      <Button
        label="Explain with setState"
        variant="primary"
        size="sm"
        stretch="auto"
        onClick={() => setOpen(true)}
      />
      <span className="text-body-sm-regular text-secondary">
        React state: {open ? "open" : "closed"}
      </span>
    </div>
  );
}
```

### Conditional tooltip

`disabled` unmounts the tooltip for a boolean condition (`disabled={!isCollapsed}` — no tip once the sidebar expands). `label` unmounts on its own when copy may be missing (`label={email}`, no `?? ""` needed) — an empty or whitespace-only string leaves the trigger with no tooltip instead of an empty popup.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { Tooltip } from "@makeplane/propel/components/tooltip";
import * as React from "react";

export default function ConditionalDemo() {
  const [isCollapsed, setIsCollapsed] = React.useState(true);
  const email: string | undefined = undefined;

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Tooltip label="Expand sidebar" disabled={!isCollapsed}>
        <Button
          label={isCollapsed ? "Collapsed" : "Expanded"}
          variant="secondary"
          size="sm"
          stretch="auto"
          onClick={() => setIsCollapsed((value) => !value)}
        />
      </Tooltip>
      <Tooltip label={email}>
        <Button label="Member (no email)" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
    </div>
  );
}
```

### Imperative handle

Use when you do not want to lift `useState`. `createTooltipHandle()` lives outside the tree; pass it as `handle`, put a stable `id` on the trigger child, then call `handle.open(id)` (here, “Open tip via handle.open”). No React open state — the handle talks to Base UI directly. Calling `open` while the tooltip is gated off by `disabled` or a missing label is a no-op. Multiple detached triggers still need Base UI `Tooltip.Trigger` composition; the ready-made only mounts one trigger from `children`.

```tsx
import { Button } from "@makeplane/propel/components/button";
import { createTooltipHandle, Tooltip } from "@makeplane/propel/components/tooltip";

const SHARE_TRIGGER_ID = "docs-share-project-hint";
const shareHint = createTooltipHandle();

export default function WithHandleDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Tooltip handle={shareHint} label="Anyone with the link can view this project">
        <Button id={SHARE_TRIGGER_ID} label="Share" variant="secondary" size="sm" stretch="auto" />
      </Tooltip>
      <Button
        label="Open tip via handle.open"
        variant="primary"
        size="sm"
        stretch="auto"
        onClick={() => shareHint.open(SHARE_TRIGGER_ID)}
      />
      <span className="text-body-sm-regular text-secondary">trigger id: {SHARE_TRIGGER_ID}</span>
    </div>
  );
}
```
## API Reference

`createTooltipHandle()` (from the same entry) returns a handle for imperative `open(triggerId)` /
`close()` — see [Imperative handle](#imperative-handle). It is a factory, not a component, so it has
no props table. `TooltipPopup` and `TooltipArrow` are the styled parts for hand-assembled tooltips
(graft Base UI behavior onto them via `render`).

### Tooltip

A small popup that describes the element it is attached to. Appears on hover or keyboard focus of
the trigger and is dismissed on blur, pointer-leave, or `Esc`.

Built on Base UI's tooltip, so it's accessible by default: the popup is exposed as
`role="tooltip"` and wired to the trigger. Pass the trigger as `children`, the popup copy as
`label`, and an optional `shortcut` for a dimmed visual keyboard hint.

Grafts Base UI's tooltip behavior onto propel's styled parts: `Tooltip.Root` + `Tooltip.Trigger`
+ `Tooltip.Portal` → `Tooltip.Positioner` (shared `internal/positioner`) → `TooltipPopup`, with
an optional `TooltipArrow` when `showArrow` is set. Colors come from propel's adaptive surface
tokens (`bg-layer-2` / `text-primary` / `border-subtle-1`), so the tooltip is light on light
themes and dark on dark themes, matching the Figma "Tooltip" component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string \| null \| undefined` | — | The text shown inside the tooltip popup. `null` / `undefined` / `""` (or whitespace-only) unmounts the popup itself (children still render, unaffected) — use this instead of `label={x ?? ""}` for copy that may be missing (an email, a name). |
| `children` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The element the tooltip is attached to. Base UI renders the trigger as a `<button>` by default; pass a single element and it is used as the trigger via the `render` prop, so any focusable element (a real button, an icon button) can anchor the tooltip. Give the element an `id` when using `createTooltipHandle` — `handle.open(id)` looks up that trigger id (a no-op while the tooltip is gated off by `disabled` or a missing label — no popup mounts to open). Custom components must forward the ref, event handlers, and aria attributes to the same DOM element. The description is attached only while the popup is mounted; existing descriptions are preserved. |
| `disabled` | `boolean` | `false` | When `true`, no popup ever mounts for this trigger — not merely closed. Use this for a boolean gate (e.g. only show a hint while a sidebar is collapsed). Passing Base UI's own `disabled` directly still leaves the popup markup mounted (just visually closed), so it doesn't give the same guarantee on its own — this prop drives that same Base UI `disabled` internally _and_ gates the popup markup. |
| `shortcut` | `string` | — | Optional keyboard-shortcut hint, dimmed (e.g. `"⌘ K"`). Sits inline-end of `label` when `layout="single"`, or beneath it when `layout="stacked"`. Maps to the Figma "Cmd + K" slot — omit it for a plain tooltip. This is visual text; put the canonical `aria-keyshortcuts` value on the trigger element. |
| `layout` | `"single" \| "stacked"` | `single` | Content arrangement. `single` is the compact Figma chip and wraps long labels within 500px; `stacked` is the multiline description layout (body type, larger radius, 500px maximum width, shortcut below). Longer content belongs in a popover. |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-end" \| "inline-start"` | `top` | Which side of the trigger to place the popup on. May flip automatically to stay in view. |
| `sideOffset` | `number` | `8` | Gap in pixels between the trigger and the popup. Increase when using `showArrow` so the caret has room between the trigger and the surface. |
| `align` | `"center" \| "start" \| "end"` | `"center"` | Alignment of the popup relative to the trigger along `side`. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset in px along the align axis — slides the popup along the trigger's edge without changing `align`. |
| `collisionPadding` | `Padding` | `5` | Minimum gap in px between the popup and the viewport edge it would otherwise touch. |
| `collisionBoundary` | `Boundary` | — | The element or rect the popup is confined to when avoiding collisions. Base UI's clipping ancestors when unset. |
| `collisionAvoidance` | `CollisionAvoidance` | — | What to do when the popup would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset. |
| `showArrow` | `boolean` | `false` | Render the caret pointing back at the trigger. Off by default — the Figma tooltip is a flat surface with no built-in caret. |
| `arrowPadding` | `number` | `5` | Minimum distance in px between the arrow and the popup's rounded corners. Ignored when `showArrow` is false. |
| `sticky` | `boolean` | `false` | Keep the popup on screen after the trigger scrolls out of view, instead of following it out. Base UI may still set `data-anchor-hidden` when the anchor is clipped — that attribute is independent of `sticky`. When `sticky` is false (default), this tooltip's positioner opts into `hideWhenAnchorHidden` so `data-anchor-hidden` makes the tip `invisible` and it does not linger. When `sticky` is true, the positioner stays visible even if the anchor is marked hidden. |
| `positionMethod` | `"absolute" \| "fixed"` | `"absolute"` | Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors, e.g. a trigger inside a sticky header. |
| `anchor` | `Element \| VirtualElement \| RefObject<Element \| null> \| (() => Element \| VirtualElement \| null) \| null` | — | Position against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one (e.g. a chart's pointer position). Geometry only; hover and focus stay on the trigger. |
| `disableAnchorTracking` | `boolean` | `false` | Stop tracking layout shifts of the anchor — position once, on open. |
| `delay` | `number` | `300` | How long to wait (ms) before opening on hover. Focus opens immediately. Defaults to propel's own `300` rather than Base UI's `600`, which read as a stall on hints people hover deliberately. Because that default is always passed, a wrapping `TooltipProvider`'s `delay` does not reach a ready-made `Tooltip` — set the timing here instead. Close timing is not on this prop: use the provider's `closeDelay` (and shared grouping) when adjacent tooltips should linger or swap without restarting the open delay. |

### TooltipProvider

Shares one open/close delay across every tooltip beneath it, so moving between nearby triggers
can show adjacent tooltips immediately.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delay` | `number` | — | How long to wait before opening a tooltip, in milliseconds. Applied only when a child trigger does not set its own `delay` — Base UI resolves `delay ?? providerDelay ?? 600` (per-tooltip wins). The ready-made `Tooltip` always passes its own `300`, so this reaches only a hand-composed `BaseTooltip.Trigger`; set the timing on the `Tooltip` itself instead. |
| `closeDelay` | `number` | — | How long to wait before closing a tooltip, in milliseconds. Shared across the group so moving between nearby triggers can swap hints without a hard cut. Per-tooltip close delay is not on the ready-made `Tooltip` — set it here. No provider default — omit for Base UI's per-trigger close delay (`0`). |
| `timeout` | `number` | `400` | After a tooltip in this group closes, another may open instantly if its trigger is interacted with within this window (ms). |
| `children` | `ReactNode` | — | The subtree whose tooltips share this group's open/close timing. |

### TooltipPopup

The styled tooltip surface — adaptive popup with caption/body text, radius, shadow, and the gap
to the shortcut. `layout` switches Figma singleline vs multiline chrome. Base-UI-agnostic; graft
in `components` via `<BaseTooltip.Popup render={<TooltipPopup/>} />`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `layout` | `"single" \| "stacked"` | `single` | Content arrangement. `single` is the compact Figma chip and wraps long labels within 500px; `stacked` is the multiline description layout (body type, larger radius, 500px maximum width, shortcut below). |

### TooltipArrow

The styled tooltip caret — a rotated square clipped per `data-side` so it points back at the
trigger. Base-UI-agnostic; graft in `components` via `<BaseTooltip.Arrow render={<TooltipArrow/>}
/>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
