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

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

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.

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

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)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.
disabledbooleanfalseWhen 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.
shortcutstring—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"singleContent 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"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.
collisionBoundaryBoundary—The element or rect the popup is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidance—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.
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) | 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.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
delaynumber300How 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.

PropTypeDefaultDescription
delaynumber—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.
closeDelaynumber—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).
timeoutnumber400After a tooltip in this group closes, another may open instantly if its trigger is interacted with within this window (ms).
childrenReactNode—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/>} />.

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 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/>} />.

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.