Tooltip
A tooltip is a brief, informative message that appears when a user interacts with an element. Tooltips are usually initiated when a button is focused or hovered.
Features
- Show tooltip on hover and focus
- Hide tooltip on Esc, click, pointer down, or scroll
- Only one tooltip shows at a time
- Labeling support for screen readers via
aria-describedby - Custom show and hide delay support
- Matches native tooltip behavior with delay on hover of first tooltip and no delay on subsequent tooltips
- Supports multiple triggers sharing a single tooltip instance
Installation
Install the tooltip package:
npm install @zag-js/tooltip @zag-js/react # or yarn add @zag-js/tooltip @zag-js/react
npm install @zag-js/tooltip @zag-js/solid # or yarn add @zag-js/tooltip @zag-js/solid
npm install @zag-js/tooltip @zag-js/vue # or yarn add @zag-js/tooltip @zag-js/vue
npm install @zag-js/tooltip @zag-js/svelte # or yarn add @zag-js/tooltip @zag-js/svelte
Anatomy
Check the tooltip anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the tooltip package:
import * as tooltip from "@zag-js/tooltip"
The tooltip package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
To get tooltip working, you'll need to:
- Set up the tooltip portal (shared container for tooltips)
- Add
triggerPropsandtooltipPropsto the right elements
Then use the framework integration helpers:
import * as tooltip from "@zag-js/tooltip" import { useMachine, normalizeProps } from "@zag-js/react" 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</div> </div> )} </> ) }
import * as tooltip from "@zag-js/tooltip" import { normalizeProps, useMachine } from "@zag-js/solid" 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 ( <div> <button {...api().getTriggerProps()}>Hover me</button> <Show when={api().open}> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}>Tooltip</div> </div> </Show> </div> ) }
<script setup> import * as tooltip from "@zag-js/tooltip" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(tooltip.machine, { id: "1" }) const api = computed(() => tooltip.connect(service, normalizeProps)) </script> <template> <div> <button ref="ref" v-bind="api.getTriggerProps()">Hover me</button> <div v-if="api.open" v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()">Tooltip</div> </div> </div> </template>
<script lang="ts"> import * as tooltip from "@zag-js/tooltip" import { useMachine, normalizeProps } from "@zag-js/svelte" const id = $props.id() const service = useMachine(tooltip.machine, { id }) const api = $derived(tooltip.connect(service, normalizeProps)) </script> <button {...api.getTriggerProps()}>Hover me</button> {#if api.open} <div {...api.getPositionerProps()}> <div {...api.getContentProps()}>Tooltip</div> </div> {/if}
Customizing the timings
By default, the tooltip opens after 400ms and closes after 150ms. You can
customize this by passing the openDelay and closeDelay context properties.
const service = useMachine(tooltip.machine, { openDelay: 500, closeDelay: 200, })
Changing the placement
The tooltip uses floating-ui for dynamic
positioning. You can change the placement of the tooltip by passing the
positioning.placement context property to the machine.
const service = useMachine(tooltip.machine, { positioning: { placement: "bottom-start", }, })
Adding an arrow
To render an arrow within the tooltip, use the api.getArrowProps() and
api.getArrowTipProps().
//... const api = tooltip.connect(service, normalizeProps) //... return ( <div {...api.getPositionerProps()}> <div {...api.getArrowProps()}> <div {...api.getArrowTipProps()} /> </div> <div {...api.getContentProps()}>{/* ... */}</div> </div> ) //...
Dismiss behavior
Tooltips close on Escape, click, pointer down, and scroll by default. Configure
these with closeOnEscape, closeOnClick, closeOnPointerDown, and
closeOnScroll.
const service = useMachine(tooltip.machine, { closeOnEscape: false, closeOnClick: false, closeOnPointerDown: false, closeOnScroll: false, })
Making the tooltip interactive
Set the interactive context property to true to make the tooltip
interactive.
When a tooltip is interactive, it remains open as the pointer moves from the trigger into the content.
const service = useMachine(tooltip.machine, { interactive: true, })
Listening for open state changes
When the tooltip is opened or closed, the onOpenChange callback is invoked.
const service = useMachine(tooltip.machine, { onOpenChange(details) { // details => { open: boolean } console.log(details.open) }, })
Multiple triggers
A single tooltip instance can be shared across multiple trigger elements. Pass a
value to getTriggerProps to identify each trigger.
const service = useMachine(tooltip.machine, { onTriggerValueChange({ value }) { console.log("active trigger:", value) }, }) const api = tooltip.connect(service, normalizeProps) return ( <> <button {...api.getTriggerProps({ value: "bold" })}>B</button> <button {...api.getTriggerProps({ value: "italic" })}>I</button> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {api.triggerValue === "bold" ? "Bold" : "Italic"} </div> </div> </> )
When hovering a different trigger while the tooltip is open, it repositions
without closing. aria-describedby is scoped to the active trigger.
Controlled tooltip
Use open and onOpenChange for controlled usage.
const service = useMachine(tooltip.machine, { open, onOpenChange(details) { setOpen(details.open) }, })
Programmatic open
Use the connected API for imperative control.
api.setOpen(true)
Styling guide
Each part includes a data-part attribute you can target in CSS.
[data-part="trigger"] { /* styles for the content */ } [data-part="content"] { /* styles for the content */ }
Open and close states
When the tooltip is open, the data-state attribute is added to the trigger
[data-part="trigger"][data-state="open|closed"] { /* styles for the trigger's expanded state */ } [data-part="content"][data-state="open|closed"] { /* styles for the trigger's expanded state */ }
Styling the arrow
When using arrows within the menu, you can style it using css variables.
[data-part="arrow"] { --arrow-size: 20px; --arrow-background: red; }
Methods and Properties
Machine Context
The tooltip machine exposes the following context properties:
idsPartial<{ trigger: string | ((value?: string | undefined) => string); content: string; arrow: string; positioner: string; }> | undefinedThe ids of the elements in the tooltip. Useful for composition.openDelaynumber | undefinedThe open delay of the tooltip.closeDelaynumber | undefinedThe close delay of the tooltip.closeOnPointerDownboolean | undefinedWhether to close the tooltip on pointerdown.closeOnEscapeboolean | undefinedWhether to close the tooltip when the Escape key is pressed.closeOnScrollboolean | undefinedWhether the tooltip should close on scrollcloseOnClickboolean | undefinedWhether the tooltip should close on clickinteractiveboolean | undefinedWhether the tooltip's content is interactive. In this mode, the tooltip will remain open when user hovers over the content.onOpenChange((details: OpenChangeDetails) => void) | undefinedFunction called when the tooltip is opened.aria-labelstring | undefinedCustom label for the tooltip.positioninganyThe user provided options used to position the popover contentdisabledboolean | undefinedWhether the tooltip is disabledopenboolean | undefinedThe controlled open state of the tooltipdefaultOpenboolean | undefinedThe initial open state of the tooltip when rendered. Use when you don't need to control the open state of the tooltip.triggerValuestring | null | undefinedThe controlled trigger valuedefaultTriggerValuestring | null | undefinedThe initial trigger value when rendered. Use when you don't need to control the trigger value.onTriggerValueChange((details: TriggerValueChangeDetails) => void) | undefinedFunction called when the trigger value changes.dir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The tooltip api exposes the following methods:
openbooleanWhether the tooltip is open.setOpen(open: boolean) => voidFunction to open the tooltip.triggerValuestring | nullThe trigger valuesetTriggerValue(value: string | null) => voidFunction to set the trigger valuereposition(options?: any) => voidFunction to reposition the popover
Data Attributes
CSS Variables
Accessibility
Keyboard Interactions
- TabOpens/closes the tooltip without delay.
- EscapeIf open, closes the tooltip without delay.