# Tooltip (/docs/tooltip)





`Tooltip` is the kit's lightweight **hint** — the hover/focus counterpart to [`Popover`](/docs/popover)'s clickable panel. It's a styled layer over [Ark UI's Tooltip](https://ark-ui.com/): Ark owns the open/close state, the open/close delays, positioning, and the accessibility wiring — `role="tooltip"` on the bubble, `aria-describedby` on the trigger, `Esc` to dismiss, and **no focus trap**. The kit adds the portaled, animated, inverted bubble.

It opens on **pointer hover and keyboard focus**, so it's accessible by default.

```tsx
import { Tooltip } from '@42-central/ui-react/tooltip';

// Inline in text — children become a focusable trigger
<p>Validate the <Tooltip label="A graded project.">Norm</Tooltip> first.</p>

// Wrap an element — asChild makes the child the trigger
<Tooltip label="Delete" asChild>
  <ActionIcon aria-label="Delete"><Trash2 /></ActionIcon>
</Tooltip>
```

## On a button [#on-a-button]

The common case: wrap any interactive element with `asChild`. The tooltip names or expands on the control.

<Preview align="center" code="&#x22;<Tooltip label=\&#x22;Saved automatically every few seconds\&#x22; asChild>\n  <Button>Hover or focus me</Button>\n</Tooltip>&#x22;">
  <ButtonTooltip />
</Preview>

## On an icon-only control [#on-an-icon-only-control]

Pair it with an [`ActionIcon`](/docs/action-icon). Keep the `aria-label` on the icon — that's the accessible name; the tooltip is the visible echo for sighted users.

<Preview align="center" code="&#x22;<Tooltip label=\&#x22;Delete\&#x22; asChild>\n  <ActionIcon aria-label=\&#x22;Delete\&#x22; variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22;>\n    <Trash2 />\n  </ActionIcon>\n</Tooltip>&#x22;">
  <ActionIconTooltip />
</Preview>

## Inline in text [#inline-in-text]

Without `asChild`, the children are wrapped in a focusable inline `<span>` trigger that inherits the surrounding font — so a tooltip drops straight into a `<p>`, and the term stays **selectable, copyable prose** (a `<button>` would be excluded from text selection). Use `variant="text"` for a dotted-underline glossary look, or the default `variant="inline"` to leave the text unstyled; `classNames.trigger` overrides either. To attach a hint to an element you already have (a link, an icon button), use `asChild` so that element becomes the trigger.

<Preview align="block" code="&#x22;<p>\n  Every cadet must validate the{\&#x22; \&#x22;}\n  <Tooltip label=\&#x22;A graded project in the 42 curriculum.\&#x22; variant=\&#x22;text\&#x22;>\n    Norm\n  </Tooltip>{\&#x22; \&#x22;}\n  before moving on to the next milestone.\n</p>&#x22;">
  <InlineTooltip />
</Preview>

## Positioning [#positioning]

`position` sets the placement and `offset` the gap; the bubble flips and shifts to stay in view (Ark / Floating UI). Prefer the logical, **RTL-aware** sides `start` / `end` — they resolve to left / right based on the active direction, so the tooltip mirrors automatically under `dir="rtl"`. (Physical `left` / `right` and Ark's aligned variants like `top-start` still work.)

<Preview align="center" code="&#x22;<Tooltip label=\&#x22;Placed start\&#x22; position=\&#x22;start\&#x22; asChild>\n  <Button>start</Button>\n</Tooltip>\n<Tooltip label=\&#x22;Placed top\&#x22; position=\&#x22;top\&#x22; asChild>\n  <Button>top</Button>\n</Tooltip>\n<Tooltip label=\&#x22;Placed bottom\&#x22; position=\&#x22;bottom\&#x22; asChild>\n  <Button>bottom</Button>\n</Tooltip>\n<Tooltip label=\&#x22;Placed end\&#x22; position=\&#x22;end\&#x22; asChild>\n  <Button>end</Button>\n</Tooltip>&#x22;">
  <TooltipPlacements />
</Preview>

## With an arrow [#with-an-arrow]

Pass `withArrow` to grow a small arrow that points back at the trigger — a clearer visual tie when the bubble sits away from its anchor. It inherits the inverted surface, flips with the placement, and tucks behind the bubble so only the tip shows. In the compound form, compose a `Tooltip.Arrow` inside `Tooltip.Content` instead; restyle it with `classNames.arrow` (or the part's own `className`).

<Preview align="center" code="&#x22;<Tooltip label=\&#x22;Points back at the trigger\&#x22; withArrow asChild>\n  <Button>Hover or focus me</Button>\n</Tooltip>&#x22;">
  <ArrowTooltip />
</Preview>

## Playground [#playground]

<story.Playground />

## Anatomy [#anatomy]

* **`Tooltip`** — the convenience form: a `label` plus a single child trigger.
* **`Tooltip.Root`** — wraps Ark's `Tooltip.Root`; owns open state, delays, and positioning.
* **`Tooltip.Trigger`** — the trigger; use `asChild` to wrap your own element.
* **`Tooltip.Content`** — the portaled, animated bubble (`role="tooltip"`).
* **`Tooltip.Arrow`** — an optional arrow pointing at the trigger; place it inside `Tooltip.Content`.

`Tooltip.Root` is a headless provider with no DOM node of its own, so it takes no `testId`. `Tooltip.Content` (the bubble) and `Tooltip.Arrow` each take their own `testId`; the convenience `<Tooltip testId="...">` shorthand routes its `testId` to the rendered content bubble too. `Tooltip.Trigger` is a raw Ark re-export and was left alone: Ark already forwards an arbitrary `data-testid` straight through natively. See [Testing](/docs/testing).

```tsx
<Tooltip.Root position="end">
  <Tooltip.Trigger asChild>
    <Button>Hover</Button>
  </Tooltip.Trigger>
  <Tooltip.Content>
    <Tooltip.Arrow />
    Rich <strong>content</strong>
  </Tooltip.Content>
</Tooltip.Root>
```

## API [#api]

`Tooltip` props (the convenience form; `Tooltip.Root` takes the same props minus `label` / `asChild` / `withArrow` / `classNames`):

<TypeTable
  type="{
  label: {
    type: &#x22;ReactNode&#x22;,
    description:
      &#x22;The hint shown on hover/focus. Wired as the trigger's aria-describedby.&#x22;,
  },
  asChild: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description:
      &#x22;Use the single child as the trigger instead of wrapping it in an inline span.&#x22;,
  },
  variant: {
    type: '&#x22;text&#x22; | &#x22;inline&#x22;',
    default: '&#x22;inline&#x22;',
    description:
      'Inline trigger style: &#x22;text&#x22; adds a dotted underline (glossary term), &#x22;inline&#x22; leaves it unstyled. Ignored with asChild.',
  },
  position: {
    type: &#x22;Placement&#x22;,
    default: '&#x22;top&#x22;',
    description:
      &#x22;Bubble placement. Logical start/end are RTL-aware; also accepts top/bottom/left/right and aligned variants like top-start.&#x22;,
  },
  offset: {
    type: &#x22;number&#x22;,
    description: &#x22;Gap in px between the trigger and the bubble.&#x22;,
  },
  withArrow: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description:
      &#x22;Add a small arrow pointing at the trigger. In the compound form, compose Tooltip.Arrow inside Tooltip.Content instead.&#x22;,
  },
  openDelay: {
    type: &#x22;number&#x22;,
    default: &#x22;400&#x22;,
    description: &#x22;Delay before opening, in ms.&#x22;,
  },
  closeDelay: {
    type: &#x22;number&#x22;,
    default: &#x22;150&#x22;,
    description: &#x22;Delay before closing, in ms.&#x22;,
  },
  interactive: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Keep the bubble open while the pointer is over it.&#x22;,
  },
  withinPortal: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description:
      &#x22;Render the bubble in a Portal so it escapes clipping ancestors.&#x22;,
  },
  container: {
    type: &#x22;HTMLElement | null&#x22;,
    description: &#x22;Portal target. Defaults to document.body.&#x22;,
  },
  disabled: {
    type: &#x22;boolean&#x22;,
    description: &#x22;Prevent the tooltip from opening.&#x22;,
  },
  open: { type: &#x22;boolean&#x22;, description: &#x22;Controlled open state.&#x22; },
  defaultOpen: {
    type: &#x22;boolean&#x22;,
    description: &#x22;Uncontrolled initial open state.&#x22;,
  },
  onOpenChange: {
    type: &#x22;(details: { open: boolean }) => void&#x22;,
    description: &#x22;Fires when the tooltip opens or closes.&#x22;,
  },
  onClose: {
    type: &#x22;() => void&#x22;,
    description:
      &#x22;Fires on close (any source) — pointer/focus leaving the trigger, Escape, or a controlled onOpenChange.&#x22;,
  },
  classNames: {
    type: &#x22;{ trigger?: string; content?: string; arrow?: string }&#x22;,
    description:
      &#x22;Per-slot class overrides — the inline trigger, the bubble, and the arrow.&#x22;,
  },
  testId: {
    type: &#x22;string&#x22;,
    description:
      &#x22;Renders as `data-testid` on the content bubble (`Tooltip.Content`), not the trigger. See [Testing](/docs/testing).&#x22;,
  },
}"
/>
