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 or pointer down.
- 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.
Installation
To use the tooltip machine in your project, run the following command in your command line:
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/vue # or yarn add @zag-js/tooltip @zag-js/vue
This command will install the framework agnostic tooltip logic and the reactive utilities for your framework of choice.
Anatomy
To set up the tooltip correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the tooltip package into your project
import * as tooltip from "@zag-js/tooltip"
The tooltip package exports two key functions:
machine
— The state machine logic for the tooltip widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
To get the tooltip working correct, you'll need to:
- Setup the tooltip portal, this is a shared container for all tooltips
- Add the
triggerProps
, andtooltipProps
to the elements
You'll also need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the tooltip machine in your project 🔥
import * as tooltip from "@zag-js/tooltip" import { useMachine, normalizeProps } from "@zag-js/react" export function Tooltip() { const [state, send] = useMachine(tooltip.machine({ id: "1" })) const api = tooltip.connect(state, send, normalizeProps) return ( <> <button {...api.triggerProps}>Hover me</button> {api.isOpen && ( <div {...api.positionerProps}> <div {...api.contentProps}>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 [state, send] = useMachine(tooltip.machine({ id: createUniqueId() })) const api = createMemo(() => tooltip.connect(state, send, normalizeProps)) return ( <div> <button {...api().triggerProps}>Hover me</button> <Show when={api().isOpen}> <div {...api().positionerProps}> <div {...api().contentProps}>Tooltip</div> </div> </Show> </div> ) }
import * as tooltip from "@zag-js/tooltip" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "Tooltip", setup() { const [state, send] = useMachine(tooltip.machine({ id: "1" })) const apiRef = computed(() => tooltip.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.current return ( <> <div> <button {...api.triggerProps}>Hover me</button> {api.isOpen && ( <div {...api.positionerProps}> <div {...api.contentProps}>Tooltip</div> </div> )} </div> </> ) } }, })
<script setup> import * as tooltip from "@zag-js/tooltip"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(tooltip.machine({ id: "1" })); const api = computed(() => tooltip.connect(state.value, send, normalizeProps)); </script> <template> <div> <button ref="ref" v-bind="api.triggerProps">Hover me</button> <div v-if="isOpen" v-bind="api.positionerProps"> <div v-bind="api.contentProps">Tooltip</div> </div> </div> </template>
Customizing the timings
By default, the tooltip is designed to open after 1000ms
and close after
500ms
. You can customize this by passing the openDelay
and closeDelay
context properties.
const [state, send] = 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 [state, send] = useMachine( tooltip.machine({ positioning: { placement: "bottom-start", }, }), )
You can configure other position-related properties in the positioning
object.
Here's what the positioning API looks like:
export type PositioningOptions = { /** * The strategy to use for positioning */ strategy?: "absolute" | "fixed" /** * The initial placement of the floating element */ placement?: Placement /** * The offset of the floating element */ offset?: { mainAxis?: number; crossAxis?: number } /** * The main axis offset or gap between the reference and floating elements */ gutter?: number /** * Whether to flip the placement */ flip?: boolean /** * Whether to make the floating element same width as the reference element */ sameWidth?: boolean /** * The overflow boundary of the reference element */ boundary?: Boundary /** * Options to activate auto-update listeners */ listeners?: boolean | AutoUpdateOptions }
Adding an arrow
To render an arrow within the tooltip, use the api.arrowProps
and
api.arrowTipProps
.
//... const api = popover.connect(state, send) //... return ( <div {...api.positionerProps}> <div {...api.arrowProps}> <div {...api.arrowTipProps} /> </div> <div {...api.contentProps}>{/* ... */}</div> </div> ) //...
Pointerdown behavior
By default, the tooltip will close when the pointer is down on its trigger. To
prevent this behavior, pass the closeOnPointerDown
context property and set it
to false
.
const [state, send] = useMachine( tooltip.machine({ closeOnPointerDown: false, }), )
Closing on Esc
The tooltip is designed to close when the escape key is pressed. To prevent
this, pass the closeOnEscape
context property and set it to false
.
const [state, send] = useMachine( tooltip.machine({ closeOnEsc: false, }), )
Making the tooltip interactive
Tooltips are interactive by default. That means it'll remain open even the pointer leaves the trigger and move into the tooltip's content.
To disabled this behavior, pass the interactive
context property and set it to
false
.
Disabling this will violate the WCAG 2.1 success criterion 1.4.13
const [state, send] = useMachine( tooltip.machine({ interactive: false, }), )
Listening for open state changes
When the tooltip is opened or closed, the onOpenChange
callback is invoked.
const [state, send] = useMachine( tooltip.machine({ onOpenChange(details) { // details => { open: boolean } console.log("Tooltip", details.open) }, }), )
Styling guide
Earlier, we mentioned that each tooltip part has a data-part
attribute added
to them to select and style them in the DOM.
[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:
ids
Partial<{ trigger: string; content: string; arrow: string; positioner: string; }>
The ids of the elements in the tooltip. Useful for composition.id
string
The `id` of the tooltip.openDelay
number
The open delay of the tooltip.closeDelay
number
The close delay of the tooltip.closeOnPointerDown
boolean
Whether to close the tooltip on pointerdown.closeOnEsc
boolean
Whether to close the tooltip when the Escape key is pressed.interactive
boolean
Whether the tooltip's content is interactive. In this mode, the tooltip will remain open when user hovers over the content.onOpenChange
(details: OpenChangeDetails) => void
Function called when the tooltip is opened.aria-label
string
Custom label for the tooltip.positioning
PositioningOptions
The user provided options used to position the popover contentdisabled
boolean
Whether the tooltip is disabledopen
boolean
Whether the tooltip is openopen.controlled
boolean
Whether the tooltip is controlled by the userdir
"ltr" | "rtl"
The document's text/writing direction.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The tooltip api
exposes the following methods:
open
boolean
Whether the tooltip is open.setOpen
(open: boolean) => void
Function to open the tooltip.reposition
(options?: Partial<PositioningOptions>) => void
Function to reposition the popover
Accessibility
Keyboard Interactions
- TabOpens/closes the tooltip without delay.
- EscapeIf open, closes the tooltip without delay.
Edit this page on GitHub