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
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>
)}
</>
)
}
<script setup>
import { useMachine, normalizeProps } from "@zag-js/vue"
import * as tooltip from "@zag-js/tooltip"
import { useId, computed } from "vue"
const service = useMachine(tooltip.machine, { id: useId() })
const api = computed(() => tooltip.connect(service, normalizeProps))
</script>
<template>
<button v-bind="api.getTriggerProps()">Hover me</button>
<div v-if="api.open" v-bind="api.getPositionerProps()">
<div v-bind="api.getContentProps()">Tooltip content</div>
</div>
</template>
import { useMachine, normalizeProps } from "@zag-js/solid"
import * as tooltip from "@zag-js/tooltip"
import { createMemo, createUniqueId, Show } from "solid-js"
export function Tooltip() {
const service = useMachine(tooltip.machine, { id: createUniqueId() })
const api = createMemo(() => tooltip.connect(service, normalizeProps))
return (
<>
<button {...api().getTriggerProps()}>Hover me</button>
<Show when={api().open}>
<div {...api().getPositionerProps()}>
<div {...api().getContentProps()}>Tooltip content</div>
</div>
</Show>
</>
)
}
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
Milliseconds before the tooltip opens on hover. Defaults to 400.
Milliseconds before the tooltip closes after the pointer leaves. Defaults to 150.
Whether the tooltip content is interactive. Keeps it open while the pointer is inside. Defaults to false.
The controlled open state of the tooltip.
The initial open state (uncontrolled).
Whether the tooltip is disabled.
Whether to close when Escape is pressed. Defaults to true.
Whether to close when the trigger is clicked. Defaults to true.
Whether to close on pointer down. Defaults to true.
Whether to close when the page is scrolled. Defaults to true.
Options for the floating-ui positioner (placement, offset, flip, shift, etc.).
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 */ }
[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
| Key | Description |
|---|
Tab | Focuses the trigger; opens the tooltip (per browser behavior) |
Escape | Closes the tooltip when the trigger is focused |