Propel

Tooltip

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

Show code
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>
  );
}

Installation

import { Tooltip } from "@makeplane/propel/components/tooltip";

Usage

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.

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

Show code
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

Show code
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 one-line chip (whitespace-nowrap) — long labels stay on one line and can grow past the viewport. Use layout="stacked" for short wrapping descriptions (Figma multiline). The shortcut sits under the label. Prefer a popover if the copy runs longer than about two lines.

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

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

React state: closed
Show code
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>
  );
}

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. Multiple detached triggers still need Base UI Tooltip.Trigger composition; the ready-made only mounts one trigger from children.

trigger id: docs-share-project-hint
Show code
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. 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.

PropTypeDefaultDescription
label(required)stringThe text shown inside the tooltip popup.
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.
shortcutstringOptional 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"singleContent arrangement. `single` is the one-line Figma singleline chip (`whitespace-nowrap` — long labels do not wrap); `stacked` is the multiline description layout (body type, larger radius, soft `max-w-60`, shortcut below). Prefer `stacked` for wrapping copy — longer content belongs in a popover.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"topWhich side of the trigger to place the popup on. May flip automatically to stay in view.
sideOffsetnumber8Gap 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`.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the popup along the trigger's edge without changing `align`.
collisionPaddingPadding5Minimum gap in px between the popup and the viewport edge it would otherwise touch.
collisionBoundaryBoundaryThe element or rect the popup is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat 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.
showArrowbooleanfalseRender the caret pointing back at the trigger. Off by default — the Figma tooltip is a flat surface with no built-in caret.
arrowPaddingnumber5Minimum distance in px between the arrow and the popup's rounded corners. Ignored when `showArrow` is false.
stickybooleanfalseKeep 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.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullPosition 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.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
delaynumber600How long to wait (ms) before opening on hover. Focus opens immediately. When omitted, Base UI's trigger default (`600`) applies unless a wrapping `TooltipProvider` supplies `delay` — the per-tooltip value wins over the provider. 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.

PropTypeDefaultDescription
delaynumberHow 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). No provider default — omit to leave each trigger on Base UI's `600` open delay.
closeDelaynumberHow 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`).
timeoutnumber400After a tooltip in this group closes, another may open instantly if its trigger is interacted with within this window (ms).
childrenReactNodeThe 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/>} />`.

PropTypeDefaultDescription
renderReactElement<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"singleContent arrangement. `single` is the one-line Figma singleline chip (`whitespace-nowrap` — long labels do not wrap); `stacked` is the multiline description layout (body type, larger radius, soft max 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/>} />`.

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