Hover Card
A hover card lets sighted users preview content behind a link.
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
npm install @zag-js/hover-card @zag-js/solid # or yarn add @zag-js/hover-card @zag-js/solid
npm install @zag-js/hover-card @zag-js/vue # or yarn add @zag-js/hover-card @zag-js/vue
npm install @zag-js/hover-card @zag-js/svelte # or yarn add @zag-js/hover-card @zag-js/svelte
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> )} </> ) }
import * as hoverCard from "@zag-js/hover-card" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, Show } from "solid-js" import { Portal } from "solid-js/web" function Checkbox() { const service = useMachine(hoverCard.machine, { id: createUniqueId() }) const api = createMemo(() => hoverCard.connect(service, normalizeProps)) return ( <> <a href="https://twitter.com/zag_js" target="_blank" {...api().getTriggerProps()} > Twitter </a> <Show when={api().open}> <Portal> <div {...api().getPositionerProps()}> <div {...api().getContentProps()}> <div {...api().getArrowProps()}> <div {...api().getArrowTipProps()} /> </div> Twitter Preview </div> </div> </Portal> </Show> </> ) }
<script setup> import * as hoverCard from "@zag-js/hover-card" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport } from "vue" const service = useMachine(hoverCard.machine, { id: "1" }) const api = computed(() => hoverCard.connect(service, normalizeProps)) </script> <template> <a href="https://twitter.com/zag_js" target="_blank" v-bind="api.getTriggerProps()" > Twitter </a> <Teleport to="body" v-if="api.open"> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-bind="api.getArrowProps()"> <div v-bind="api.getArrowTipProps()" /> </div> Twitter Preview </div> </div> </Teleport> </template>
<script lang="ts"> import * as hoverCard from "@zag-js/hover-card" import { portal, useMachine, normalizeProps } from "@zag-js/svelte" const id = $props.id() const service = useMachine(hoverCard.machine, ({ id })) const api = $derived(hoverCard.connect(service, normalizeProps)) </script> <a href="https://twitter.com/zag_js" target="_blank" {...api.getTriggerProps()}> Twitter </a> {#if api.open} <div use:portal {...api.getPositionerProps()}> <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getArrowProps()}> <div {...api.getArrowTipProps()}></div> </div> Twitter Preview </div> </div> </div> {/if}
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 } console.log("hovercard is:", details.open ? "opened" : "closed") }, })
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 disabledopenboolean | undefinedThe controlled open state of the hover carddefaultOpenboolean | 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 contenttriggerValuestring | 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 | 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 componentonFocusOutside((event: FocusOutsideEvent) => void) | undefinedFunction called when the focus is moved outside the componentonInteractOutside((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 opensetOpen(open: boolean) => voidFunction to open the hover cardtriggerValuestring | nullThe trigger valuesetTriggerValue(value: string | null) => voidFunction to set the trigger valuereposition(options?: Partial<PositioningOptions>) => voidFunction to reposition the popover
Data Attributes
CSS Variables
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.
- TabOpens the hover card once focus reaches the trigger, after the open delay. Closes it when focus leaves.
- EscIf open, closes the hover card.
If you need content the user can tab into, use a popover instead.
Edit this page on GitHub