Skip to main content

A hover card lets sighted users preview content behind a link.

Loading...

Features

  • Customize side, alignment, offsets
  • Optionally render a pointing arrow
  • Supports custom open and close delays
  • Opens on hover or keyboard focus
  • Stays open while the pointer travels from the trigger to the content
  • Anchors to the hovered line when the trigger wraps across lines
  • Ignored by screen readers
  • Supports multiple triggers sharing a single hover card instance

Installation

Install the hover card package:

npm install @zag-js/hover-card @zag-js/react # or yarn add @zag-js/hover-card @zag-js/react

Anatomy

To set up the hover card correctly, you'll need to understand its anatomy and how we name its parts.

Each part includes a component-scoped data attribute to help identify it in the DOM.

Usage

Import the hover card package:

import * as hoverCard from "@zag-js/hover-card"

The hover card package exports two key functions:

  • machine - State machine logic.
  • connect - Maps machine state to JSX props and event handlers.

Then use the framework integration helpers:

import * as hoverCard from "@zag-js/hover-card" import { useMachine, normalizeProps, Portal } from "@zag-js/react" import { useId } from "react" function HoverCard() { const service = useMachine(hoverCard.machine, { id: useId() }) const api = hoverCard.connect(service, normalizeProps) return ( <> <a href="https://twitter.com/zag_js" target="_blank" {...api.getTriggerProps()} > Twitter </a> {api.open && ( <Portal> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getArrowProps()}> <div {...api.getArrowTipProps()} /> </div> Twitter Preview </div> </div> </Portal> )} </> ) }

Setting the initial state

Set defaultOpen to true to start with the hover card open.

const service = useMachine(hoverCard.machine, { defaultOpen: true, })

Controlled open state

Use open and onOpenChange to control visibility externally.

const service = useMachine(hoverCard.machine, { open, onOpenChange(details) { setOpen(details.open) }, })

Customizing open and close delays

Use openDelay and closeDelay to control hover timing.

const service = useMachine(hoverCard.machine, { openDelay: 300, closeDelay: 150, })

Positioning the hover card

Use positioning to control placement and offsets.

const service = useMachine(hoverCard.machine, { positioning: { placement: "bottom-start", offset: { mainAxis: 8, crossAxis: 4 }, }, })

Multiple triggers

A single hover card instance can be shared across multiple trigger elements. Pass a value to getTriggerProps to identify each trigger.

const users = [ { id: "1", name: "Alice", avatar: "/alice.png" }, { id: "2", name: "Bob", avatar: "/bob.png" }, ] const service = useMachine(hoverCard.machine, { onTriggerValueChange({ value }) { const user = users.find((u) => u.id === value) ?? null setActiveUser(user) }, }) const api = hoverCard.connect(service, normalizeProps) return ( <> {users.map((user) => ( <a {...api.getTriggerProps({ value: user.id })}>{user.name}</a> ))} <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> {/* Content updates based on activeUser */} </div> </div> </> )

When hovering a different trigger while the card is open, it repositions without closing.

Disabling the hover card

Set disabled to true to prevent it from opening.

const service = useMachine(hoverCard.machine, { disabled: true, })

Listening for open state changes

When the hover card is opened or closed, the onOpenChange callback is invoked.

const service = useMachine(hoverCard.machine, { onOpenChange(details) { // details => { open: boolean, reason?: OpenChangeReason } console.log("hovercard is:", details.open ? "opened" : "closed") }, })

details.reason says what caused the change, so you can treat a deliberate dismissal differently from the pointer wandering off.

ReasonMeaning
trigger-hoverThe pointer entered the trigger or content
trigger-focusThe trigger received focus
trigger-blurFocus left the trigger
pointer-leaveThe pointer left the trigger and content
interact-outsideThe user pressed outside the hover card
escape-keyThe user pressed Esc
scriptChanged programmatically, such as setOpen
const service = useMachine(hoverCard.machine, { onOpenChange(details) { if (!details.open && details.reason === "escape-key") { // the user dismissed it, so don't reopen on the next hover } }, })

Styling guide

Each part includes a component-scoped data attribute you can target in CSS.

[data-hover-card-trigger] { /* styles for trigger */ } [data-hover-card-content] { /* styles for content */ }

Open and closed state

The hover card exposes a data-state attribute that can be used to style the hover card based on its open-close state.

[data-hover-card-trigger][data-state="open|closed"] { /* styles for open or closed state */ } [data-hover-card-content][data-state="open|closed"] { /* styles for open or closed state */ }

Arrow

You can use CSS variables to style the arrow.

[data-hover-card-arrow] { /* styles for arrow */ --arrow-background: white; --arrow-size: 8px; }

Methods and Properties

Machine Context

The hover card machine exposes the following context properties:

  • idsElementIds | undefinedThe ids of the elements in the popover. Useful for composition.
  • onOpenChange((details: OpenChangeDetails) => void) | undefinedFunction called when the hover card opens or closes.
  • openDelaynumber | undefinedThe duration from when the mouse enters the trigger until the hover card opens.
  • closeDelaynumber | undefinedThe duration from when the mouse leaves the trigger or content until the hover card closes.
  • disabledboolean | undefinedWhether the hover card is disabled
  • openboolean | undefinedThe controlled open state of the hover card
  • defaultOpenboolean | undefinedThe initial open state of the hover card when rendered. Use when you don't need to control the open state of the hover card.
  • positioningPositioningOptions | undefinedThe user provided options used to position the popover content
  • triggerValuestring | null | undefinedThe controlled trigger value
  • defaultTriggerValuestring | 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 | Document | Node) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
  • onPointerDownOutside((event: PointerDownOutsideEvent) => void) | undefinedFunction called when the pointer is pressed down outside the component
  • onFocusOutside((event: FocusOutsideEvent) => void) | undefinedFunction called when the focus is moved outside the component
  • onInteractOutside((event: InteractOutsideEvent) => void) | undefinedFunction called when an interaction happens outside the component

Machine API

The hover card api exposes the following methods:

  • openbooleanWhether the hover card is open
  • setOpen(open: boolean) => voidFunction to open the hover card
  • triggerValuestring | nullThe trigger value
  • setTriggerValue(value: string | null) => voidFunction to set the trigger value
  • reposition(options?: Partial<PositioningOptions>) => voidFunction to reposition the popover

Data Attributes

Trigger
data-hover-card-trigger
<uid>
data-placement
The placement of the trigger
data-side
The side of the trigger that the trigger is positioned on
data-value
The value of the item
data-current
Present when current
data-state
"open" | "closed"
Content
data-hover-card-content
<uid>
data-state
"open" | "closed"
data-placement
The placement of the content
data-side
The side of the trigger that the content is positioned on
data-nested
popover
data-has-nested
popover

CSS Variables

Arrow
--arrow-size
The size of the arrow
--arrow-size-half
Half the size of the arrow
--arrow-background
Use this variable to style the arrow background
--arrow-offset
The offset position of the arrow
Positioner
--reference-width
The width of the reference element
--reference-height
The height of the root
--available-width
The available width in viewport
--available-height
The available height in viewport
--x
The x position for transform
--y
The y position for transform
--z-index
The z-index value
--transform-origin
The transform origin for animations
Content
--layer-index
The index of the dismissable in the layer stack
--nested-layer-count
The number of nested hover-cards
Backdrop
--layer-index
The index of the dismissable in the layer stack

Accessibility

The hover card is a progressive enhancement for sighted users. Its content is not reachable by keyboard or exposed to screen readers, so treat it as a preview of something the user can already get to another way.

Keep the content non-essential and non-interactive. Anything the user must be able to read or act on belongs on the page the trigger links to, not only in the card.

Keyboard Interactions

The trigger is a normal focusable element. Focusing it opens the card so keyboard users see the same preview, but focus never enters the content.

  • Tab
    Opens the hover card once focus reaches the trigger, after the open delay. Closes it when focus leaves.
  • Esc
    If open, closes the hover card.

If you need content the user can tab into, use a popover instead.

Edit this page on GitHub