Skip to main content

A popover is a non-modal dialog that floats around a trigger. It is used to display contextual information to the user, and should be paired with a clickable trigger element.

Loading...

Features

  • Focus is managed and can be customized
  • Supports modal and non-modal modes
  • Ensures correct DOM order after tabbing out of the popover, whether it's portalled or not

Installation

Install the popover package:

npm install @zag-js/popover @zag-js/vue # or yarn add @zag-js/popover @zag-js/vue

Anatomy

Check the popover anatomy and part names.

Each part includes a data-part attribute to help identify them in the DOM.

Usage

Import the popover package:

import * as popover from "@zag-js/popover"

The popover package exports two key functions:

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

Pass a unique id to useMachine so generated element ids stay predictable.

Then use the framework integration helpers:

<script setup> import * as popover from "@zag-js/popover" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport, Fragment } from "vue" const service = useMachine(popover.machine, { id: "1" }) const api = computed(() => popover.connect(service, normalizeProps)) </script> <template> <div ref="ref"> <button v-bind="api.getTriggerProps()">Click me</button> <Teleport to="body" :disabled="!api.portalled"> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-bind="api.getTitleProps()">Presenters</div> <div v-bind="api.getDescriptionProps()">Description</div> <button>Action Button</button> <button v-bind="api.getCloseTriggerProps()">X</button> </div> </div> </Teleport> </div> </template>

Rendering the popover in a portal

By default, the popover is rendered in the same DOM hierarchy as the trigger. To render the popover within a portal, pass portalled: true property to the machine's context.

Note: This requires that you render the component within a Portal based on the framework you use.

<script setup> import * as popover from "@zag-js/popover" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, Teleport } from "vue" const service = useMachine(popover.machine, { id: "1" }) const api = computed(() => popover.connect(service, normalizeProps)) </script> <template> <div ref="ref"> <button v-bind="api.getTriggerProps()">Click me</button> <Teleport to="body"> <div v-bind="api.getPositionerProps()"> <div v-bind="api.getContentProps()"> <div v-bind="api.getTitleProps()">Presenters</div> <div v-bind="api.getDescriptionProps()">Description</div> <button>Action Button</button> <button v-bind="api.getCloseTriggerProps()">X</button> </div> </div> </Teleport> </div> </template>

Managing focus within popover

When the popover opens, focus moves to the first focusable element. To customize this, set initialFocusEl.

<script setup> import { ref } from "vue" // initial focused element ref const inputRef = ref(null) const service = useMachine(popover.machine, { initialFocusEl: () => inputRef.value, }) </script> <template> <input ref="inputRef" /> </template>

To disable automatic focus on open, set autoFocus to false.

const service = useMachine(popover.machine, { autoFocus: false, })

Changing the modality

In some cases, you might want the popover to be modal. This means that it'll:

  • trap focus within its content
  • block scrolling on the body
  • disable pointer interactions outside the popover
  • hide content behind the popover from screen readers

To make the popover modal, set the modal: true property in the machine's context. When modal: true, we set the portalled attribute to true as well.

Note: This requires that you render the component within a Portal.

const service = useMachine(popover.machine, { modal: true, })

Close behavior

The popover is designed to close on blur and when the esc key is pressed.

To prevent it from closing on blur (clicking or focusing outside), pass the closeOnInteractOutside property and set it to false.

const service = useMachine(popover.machine, { closeOnInteractOutside: false, })

To prevent it from closing when the esc key is pressed, pass the closeOnEscape property and set it to false.

const service = useMachine(popover.machine, { closeOnEscape: false, })

Controlled open state

Use open and onOpenChange to control visibility externally.

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

Adding an arrow

To render an arrow within the popover, use the api.getArrowProps() and api.getArrowTipProps().

//... const api = popover.connect(service, normalizeProps) //... return ( <div {...api.getPositionerProps()}> <div {...api.getContentProps()}> <div {...api.getArrowProps()}> <div {...api.getArrowTipProps()} /> </div> //... </div> </div> ) //...

Changing the placement

To change the placement of the popover, set the positioning.placement property in the machine's context.

const service = useMachine(popover.machine, { positioning: { placement: "top-start", }, })

Listening for open state changes

When the popover is opened or closed, the onOpenChange callback is invoked.

const service = useMachine(popover.machine, { onOpenChange(details) { // details => { open: boolean } console.log("Popover", details.open) }, })

Usage within dialog

When using the popover within a dialog, avoid rendering the popover in a Portal or Teleport. This is because the dialog will trap focus within it, and the popover will be rendered outside the dialog.

Styling guide

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

Open and closed state

When the popover is expanded, we add a data-state and data-placement attribute to the trigger.

[data-part="trigger"][data-state="open|closed"] { /* styles for the expanded state */ } [data-part="content"][data-state="open|closed"] { /* styles for the expanded state */ } [data-part="trigger"][data-placement="(top|bottom)-(start|end)"] { /* styles for computed placement */ }

Position aware

When the popover is expanded, data-placement is added to trigger and content.

[data-part="trigger"][data-placement="(top|bottom)-(start|end)"] { /* styles for computed placement */ } [data-part="content"][data-placement="(top|bottom)-(start|end)"] { /* styles for computed placement */ }

Arrow

The arrow element requires specific css variables to be set for it to show correctly.

[data-part="arrow"] { --arrow-background: white; --arrow-size: 16px; }

A common technique for adding a shadow to the arrow is to use filter: drop-shadow(...) on the content element. Alternatively, you can use the --arrow-shadow-color variable.

[data-part="arrow"] { --arrow-shadow-color: gray; }

Methods and Properties

Machine Context

The popover machine exposes the following context properties:

  • idsPartial<{ anchor: string; trigger: string; content: string; title: string; description: string; closeTrigger: string; positioner: string; arrow: string; }>The ids of the elements in the popover. Useful for composition.
  • modalbooleanWhether the popover should be modal. When set to `true`: - interaction with outside elements will be disabled - only popover content will be visible to screen readers - scrolling is blocked - focus is trapped within the popover
  • portalledbooleanWhether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position of the popover content.
  • autoFocusbooleanWhether to automatically set focus on the first focusable content within the popover when opened.
  • initialFocusEl() => HTMLElementThe element to focus on when the popover is opened.
  • closeOnInteractOutsidebooleanWhether to close the popover when the user clicks outside of the popover.
  • closeOnEscapebooleanWhether to close the popover when the escape key is pressed.
  • onOpenChange(details: OpenChangeDetails) => voidFunction invoked when the popover opens or closes
  • positioningPositioningOptionsThe user provided options used to position the popover content
  • openbooleanThe controlled open state of the popover
  • defaultOpenbooleanThe initial open state of the popover when rendered. Use when you don't need to control the open state of the popover.
  • idstringThe unique identifier of the machine.
  • getRootNode() => Node | ShadowRoot | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
  • dir"ltr" | "rtl"The document's text/writing direction.
  • onEscapeKeyDown(event: KeyboardEvent) => voidFunction called when the escape key is pressed
  • onRequestDismiss(event: LayerDismissEvent) => voidFunction called when this layer is closed due to a parent layer being closed
  • onPointerDownOutside(event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component
  • onFocusOutside(event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component
  • onInteractOutside(event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component
  • persistentElements(() => Element)[]Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event

Machine API

The popover api exposes the following methods:

  • portalledbooleanWhether the popover is portalled.
  • openbooleanWhether the popover is open
  • setOpen(open: boolean) => voidFunction to open or close the popover
  • reposition(options?: Partial<PositioningOptions>) => voidFunction to reposition the popover

Data Attributes

Trigger
data-scope
popover
data-part
trigger
data-placement
The placement of the trigger
data-state
"open" | "closed"
Indicator
data-scope
popover
data-part
indicator
data-state
"open" | "closed"
Content
data-scope
popover
data-part
content
data-state
"open" | "closed"
data-nested
popover
data-has-nested
popover
data-expanded
Present when expanded
data-placement
The placement of the content

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 popovers
Backdrop
--layer-index
The index of the dismissable in the layer stack

Accessibility

Adheres to the Dialog WAI-ARIA design pattern.

Keyboard Interactions

  • Space
    Opens/closes the popover.
  • Enter
    Opens/closes the popover.
  • Tab
    Moves focus to the next focusable element within the content.
    Note: If there are no focusable elements, focus is moved to the next focusable element after the trigger.
  • Shift + Tab
    Moves focus to the previous focusable element within the content
    Note: If there are no focusable elements, focus is moved to the trigger.
  • Esc
    Closes the popover and moves focus to the trigger.
Edit this page on GitHub