Skip to main content
A tooltip is a brief, informative message that appears when a user hovers or focuses an element. It follows the native browser tooltip behavior — the first tooltip has a delay, and subsequent tooltips appear immediately while the pointer moves between triggers. Features
  • Shows on hover and focus
  • Hides on Escape, click, pointer down, and scroll
  • Only one tooltip visible at a time (shared delay group)
  • Screen reader support via aria-describedby
  • Configurable open and close delays
  • Optional arrow with CSS variable styling
  • Interactive mode keeps tooltip open when pointer enters content

Installation

npm install @zag-js/tooltip @zag-js/react

Usage

import { useMachine, normalizeProps } from "@zag-js/react"
import * as tooltip from "@zag-js/tooltip"
import { useId } from "react"

export function Tooltip() {
  const service = useMachine(tooltip.machine, { id: useId() })
  const api = tooltip.connect(service, normalizeProps)

  return (
    <>
      <button {...api.getTriggerProps()}>Hover me</button>
      {api.open && (
        <div {...api.getPositionerProps()}>
          <div {...api.getContentProps()}>
            Tooltip content
          </div>
        </div>
      )}
    </>
  )
}

Adding an arrow

Render the arrow inside the positioner, before the content. Style with CSS variables.
<div {...api.getPositionerProps()}>
  <div {...api.getArrowProps()}>
    <div {...api.getArrowTipProps()} />
  </div>
  <div {...api.getContentProps()}>Tooltip content</div>
</div>

Customizing delays

The tooltip opens after 400ms and closes after 150ms by default.
const service = useMachine(tooltip.machine, {
  id: useId(),
  openDelay: 600,
  closeDelay: 300,
})

Changing placement

Use positioning.placement to control where the tooltip appears relative to the trigger.
const service = useMachine(tooltip.machine, {
  id: useId(),
  positioning: {
    placement: "bottom-start",
  },
})
Supported placements: "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end".

Interactive tooltips

Set interactive: true to keep the tooltip open as the pointer moves from the trigger into the content. Required for tooltips that contain links or interactive elements.
const service = useMachine(tooltip.machine, {
  id: useId(),
  interactive: true,
})

Dismiss behavior

Control which interactions close the tooltip.
const service = useMachine(tooltip.machine, {
  id: useId(),
  closeOnEscape: false,
  closeOnClick: false,
  closeOnPointerDown: false,
  closeOnScroll: false,
})

Controlled open state

Use open and onOpenChange to control visibility externally.
const service = useMachine(tooltip.machine, {
  id: useId(),
  open,
  onOpenChange(details) {
    setOpen(details.open)
  },
})

Custom aria-label

By default, aria-describedby links the trigger to the tooltip content. To use a tooltip as an accessible name instead, provide aria-label.
const service = useMachine(tooltip.machine, {
  id: useId(),
  "aria-label": "More information about pricing",
})

Disabled tooltip

const service = useMachine(tooltip.machine, {
  id: useId(),
  disabled: true,
})

API reference

openDelay
number
Milliseconds before the tooltip opens on hover. Defaults to 400.
closeDelay
number
Milliseconds before the tooltip closes after the pointer leaves. Defaults to 150.
interactive
boolean
Whether the tooltip content is interactive. Keeps it open while the pointer is inside. Defaults to false.
open
boolean
The controlled open state of the tooltip.
defaultOpen
boolean
The initial open state (uncontrolled).
disabled
boolean
Whether the tooltip is disabled.
closeOnEscape
boolean
Whether to close when Escape is pressed. Defaults to true.
closeOnClick
boolean
Whether to close when the trigger is clicked. Defaults to true.
closeOnPointerDown
boolean
Whether to close on pointer down. Defaults to true.
closeOnScroll
boolean
Whether to close when the page is scrolled. Defaults to true.
positioning
PositioningOptions
Options for the floating-ui positioner (placement, offset, flip, shift, etc.).
aria-label
string
A custom accessible label for the tooltip. When provided, uses aria-label instead of aria-describedby.
onOpenChange
(details: { open: boolean }) => void
Callback fired when the tooltip opens or closes.

Styling

[data-part="trigger"] { /* the trigger element */ }
[data-part="positioner"] { /* the floating positioner */ }
[data-part="content"] { /* the tooltip content */ }

Open and closed states

[data-part="trigger"][data-state="open"] { /* trigger while tooltip is open */ }
[data-part="trigger"][data-state="closed"] { /* trigger while tooltip is closed */ }
[data-part="content"][data-state="open"] { /* visible content */ }
[data-part="content"][data-state="closed"] { /* hidden content */ }

Arrow

[data-part="arrow"] {
  --arrow-size: 12px;
  --arrow-background: #111;
}

Accessibility

The trigger element receives aria-describedby pointing to the tooltip content, which allows screen readers to announce the tooltip text when the trigger is focused.

Keyboard interactions

KeyDescription
TabFocuses the trigger; opens the tooltip (per browser behavior)
EscapeCloses the tooltip when the trigger is focused

Build docs developers (and LLMs) love