Skip to content
Nizivo UI

Popover

A small dialog anchored to its trigger, for status, quick settings and filters. It has a title, a description, any controls, and a close button; Escape or a press outside closes it, and focus returns to the trigger.

Client componentEngine: react-aria

Import

import { Popover } from '@nizivo/react/popover';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/icon-button.css
import '@nizivo/react/css/popover.css';

Static or interactive

Client. It always ships React Aria's dialog, popover and positioning code: @nizivo/react/popover measures 35.0 KiB gzip with React Aria inside (size-limit, all locales; packages/react/SIZES.md). Most of it is shared with Menu and Select, so a page that already has one of them pays far less. How to choose.

Popover, Menu, Tooltip or Dialog

  • A Menu lists actions or links; a Select chooses a value. Use them when the popup is a list.
  • A Tooltip names or explains its trigger in a few words and holds nothing to press.
  • A Popover holds content and controls next to the control that opened it: a store's status with a "Pause" action, quick filters, a notifications list.
  • A Dialog asks for the whole page's attention. Use it for a task the user must finish or dismiss before going on.

Placement

Popover.Popup takes side (top, bottom, start, end; default bottom), align (start, center, end; default center) and sideOffset (default 8 px). start and end follow the writing direction. When there is no room, it flips to the other side; data-side on the popup is the side it ended on, as a physical top, right, bottom or left, and data-align the alignment.

It is as wide as its content, up to 22.5rem (360 px) and the viewport less 32 px. Set --nz-popover-width for a fixed width. Its content scrolls when it is taller than the room below (or above) the trigger.

There is no arrow: Figma's popups (the Dropdown Menu and the Date Range Picker) have none.

Naming it

A Popover.Title names the popover. Without one, give Popover.Popup an aria-label:

<Popover.Popup aria-label="Filters">…</Popover.Popup>

The close button

<Popover.Close /> with no render and no children is a small IconButton (tertiary, sm) showing the close glyph, named "Close" (labels={{ close: '…' }} or aria-label renames it). As a direct child of Popover.Popup it sits in the top end corner, and the title leaves room for it. With children it is a plain button; with render, your element, for example render={<Button size="sm">Done</Button>}. Its markup is IconButton's, so a page that imports per-component stylesheets needs css/icon-button.css too.

Inside other overlays

A Popover opened inside a Dialog or a Drawer portals into that overlay and paints above it, and a Select, Menu, Tooltip or Dialog opened inside a Popover paints above the Popover. Escape and a press outside close only the topmost one; the page stays scroll-locked until the last one closes, and focus walks back trigger by trigger.

While it is open the rest of the page is inert and does not scroll (React Aria's popovers are modal), as with Menu and Select.

Examples

Basic

A notifications-style popover from a composed Button: a title, a description, an action, and the default close in the corner. Escape, an outside press or the close button close it, and focus returns to the trigger. Opened, it focuses its first control (the corner close is skipped).

import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Popover } from '@nizivo/react/popover';

export function PopoverExample(): JSX.Element {
  return (
    <Popover.Root>
      <Popover.Trigger render={<Button variant="secondary">Store status</Button>} />
      <Popover.Popup>
        <Popover.Title level={3}>Store is live</Popover.Title>
        <Popover.Close />
        <Popover.Description>
          Customers can see 42 products. Pause the store to hide them while you restock.
        </Popover.Description>
        <Button size="sm">Pause store</Button>
      </Popover.Popup>
    </Popover.Root>
  );
}

Open

Open on the server: closed overlays and server-rendered opens both emit no popup HTML (§12.2 SSR). The popup opens after hydration, placed at the trigger's end.

Source only

It opens on page load (defaultOpen), which would move focus on this page, so it is shown as source only.
import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Popover } from '@nizivo/react/popover';

export function PopoverOpenExample(): JSX.Element {
  return (
    <Popover.Root defaultOpen>
      <Popover.Trigger render={<Button variant="secondary">Filters</Button>} />
      <Popover.Popup side="end" align="start" aria-label="Filters">
        <Popover.Description>No filters applied.</Popover.Description>
        <Popover.Close render={<Button size="sm">Done</Button>} />
      </Popover.Popup>
    </Popover.Root>
  );
}

Parts

Use the parts through the namespace (Popover.Root) or by their flat names (PopoverRoot); both work from Server Components.

Popover.Root

Client component Renders no element of its own. Props: PopoverRootProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
childrenrequiredReactNode–
defaultOpenboolean–
onOpenChange(open: boolean, details: ChangeDetails<OpenReason>) => void–

details.reason is 'unknown' with React Aria, which reports no cause.

openboolean–

Popover.Trigger

Without render, its own <button>: nz-hit-target gives it the 44 px coarse-pointer hit area of every standalone pressable, and popover.css gives that bare button a 24 x 24 px minimum (WCAG 2.2 2.5.8, as Dialog's and Tooltip's, M27). A composed Button or IconButton keeps its own size.

Client component Renders data-slot="popover-trigger" with the class nz-popover-trigger. Props: PopoverTriggerProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
childrenReactNode–
renderRenderProp<PopoverTriggerState>–

Must render a <button>, for example render={<IconButton label="Notifications" … />}.

  • Also takes every attribute of <button> except children. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (PopoverTriggerState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
openrequiredboolean–
pressedrequiredboolean–

Popover.Popup

Client component Renders data-slot="popover-popup" with the class nz-popover-popup. Props: PopoverPopupProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
alignAlign
"start""center""end"
'center'
aria-describedbystring–
aria-labelstring–

Names the popup when it has no Popover.Title.

aria-labelledbystring–
childrenrequiredReactNode–
classNamestring–
containerElement | null–

Portal container. Defaults to the parent overlay, else document.body (§12.1 L2).

sidePopoverSide
"top""bottom""start""end"
'bottom'
sideOffsetnumber8

Distance from the trigger in px.

styleCSSProperties–

Popover.Title

Client component Renders data-slot="popover-title" with the class nz-popover-title. Props: PopoverTitleProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
levelPopoverTitleLevel
23456
2
renderRenderProp<Record<string, never>>–
  • Also takes every attribute of <h2> except id. (Omit<ComponentPropsWithRef<"h2">, "id">)

render takes an element, or, in client code, a function of the props and no state.

Popover.Description

Client component Renders data-slot="popover-description" with the class nz-popover-description. Props: PopoverDescriptionProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
renderRenderProp<Record<string, never>>–
  • Also takes every attribute of <p> except id. (Omit<ComponentPropsWithRef<"p">, "id">)

render takes an element, or, in client code, a function of the props and no state.

Popover.Close

With render or children, the consumer's button. With neither, an IconButton (tertiary, sm) showing the core close glyph, named by labels.close: the IconButton element's markup written out, as Dialog.Close does, so the Popover entry does not pull in IconButton's tooltip chain.

Client component Renders data-slot="popover-close" with the class nz-popover-close. Props: PopoverCloseProps. Source: src/popover/popover.client.tsx.

NameTypeDefaultDescription
childrenReactNode–

Text content, rendered in a plain <button>; leave it out for the default IconButton.

labelsPartial<Pick<PopoverLabels, "close">>–

The default IconButton's accessible name; an aria-label you pass wins.

renderRenderProp<PopoverCloseState>–

Must render a <button>, for example render={<Button size="sm">Done</Button>}. With neither render nor children, the part is a small IconButton showing the close glyph, named by labels.close, which sits in the popup's top corner when it is a direct child of Popover.Popup.

  • Also takes every attribute of <button> except children. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (PopoverCloseState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
pressedrequiredboolean–

Other exports

PopoverCloseProps type

import type { PopoverCloseProps } from '@nizivo/react/popover'

The props of Popover.Close, listed above.

PopoverCloseState type

import type { PopoverCloseState } from '@nizivo/react/popover'

The render state of Popover.Close, listed above.

PopoverDescriptionProps type

import type { PopoverDescriptionProps } from '@nizivo/react/popover'

The props of Popover.Description, listed above.

PopoverLabels type

import type { PopoverLabels } from '@nizivo/react/popover'

Strings the Popover renders (§9.3 rule 7, §16.4). close is the accessible name of the default Popover.Close (an IconButton showing the close glyph), set through that part's labels. Engine-owned with React Aria (not overridable): dismiss, the visually hidden "Dismiss" buttons at both ends of the popup.

NameTypeDefaultDescription
closerequiredstring'Close'
dismissrequiredstring–
PopoverPopupProps type

import type { PopoverPopupProps } from '@nizivo/react/popover'

The props of Popover.Popup, listed above.

PopoverPopupState type

import type { PopoverPopupState } from '@nizivo/react/popover'

NameTypeDefaultDescription
enteringrequiredboolean–
exitingrequiredboolean–
siderequiredSide
"top""right""bottom""left"
–
PopoverRootProps type

import type { PopoverRootProps } from '@nizivo/react/popover'

The props of Popover.Root, listed above.

PopoverSide type

import type { PopoverSide } from '@nizivo/react/popover'

Logical side; the resolved physical side is data-side on the popup.

type PopoverSide = "top" | "bottom" | "start" | "end"

PopoverTitleLevel type

import type { PopoverTitleLevel } from '@nizivo/react/popover'

type PopoverTitleLevel = 2 | 3 | 4 | 5 | 6

PopoverTitleProps type

import type { PopoverTitleProps } from '@nizivo/react/popover'

The props of Popover.Title, listed above.

PopoverTriggerProps type

import type { PopoverTriggerProps } from '@nizivo/react/popover'

The props of Popover.Trigger, listed above.

PopoverTriggerState type

import type { PopoverTriggerState } from '@nizivo/react/popover'

The render state of Popover.Trigger, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
icon-button-icon
.nz-icon-button-icon
Popover.Close
inner element (CSS only)
none
popover-close
.nz-popover-close
Popover.Close
root (takes render); interactive, focus: self
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-pressed (present or absent)
    Pointer or keyboard press (native parts use :active)
popover-content
.nz-popover-content
Popover.Popup
inner element (CSS only)
none
popover-description
.nz-popover-description
Popover.Description
root (takes render)
none
popover-popup
.nz-popover-popup
Popover.Popup
root (no render); forced colours: border
  • data-align: "start""center""end" ← align
    Resolved alignment
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-open (present or absent)
    Open
  • data-side: "top""right""bottom""left"
    Resolved physical side after flip and RTL; the side prop takes logical values
popover-title
.nz-popover-title
Popover.Title
root (takes render)
none
popover-trigger
.nz-popover-trigger
Popover.Trigger
root (takes render); interactive, focus: self
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-open (present or absent)
    Open
  • data-pressed (present or absent)
    Pointer or keyboard press (native parts use :active)

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-popover-bg--nz-bg-surface-raised
  • background on .nz-popover-popup
--nz-popover-border--nz-border-subtle
  • border on .nz-popover-popup
--nz-popover-radius--nz-radius-lg
  • border-radius on .nz-popover-popup
--nz-popover-shadow--nz-elevation-3
  • box-shadow on .nz-popover-popup
--nz-popover-widthmax-content
  • inline-size on .nz-popover-popup

Layers: nz.components. Classes: .nz-hit-target, .nz-icon-button, .nz-popover-close, .nz-popover-content, .nz-popover-description, .nz-popover-popup, .nz-popover-title, .nz-popover-trigger. Runtime variables: --nz-available-height, --nz-transform-origin.

Tokens this stylesheet reads (22)

--nz-bg-surface-raised, --nz-border-subtle, --nz-border-width-default, --nz-duration-fade, --nz-duration-spring, --nz-easing-enter, --nz-easing-exit, --nz-easing-spring, --nz-elevation-3, --nz-font-body-m, --nz-font-heading-h4, --nz-icon-secondary, --nz-radius-lg, --nz-scale-enter, --nz-space-2, --nz-space-4, --nz-space-6, --nz-text-primary, --nz-text-secondary, --nz-tracking-body-m, --nz-tracking-heading-h4, --nz-z-overlay

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • popover-close: draws the focus outline itself on keyboard focus.
  • popover-trigger: draws the focus outline itself on keyboard focus.

In forced-colours mode these states keep a visible system colour: popover-popup (border).

Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.

Pattern: WAI-ARIA Authoring Practices.

Keyboard

KeysAction
Enter, SpaceOn the trigger (a button), opens the popover and moves focus to its first control (the close button in the corner is passed over), or to the popover itself when it has none. An element with autoFocus wins.
Tab, Shift+TabMoves focus between the controls inside the popover; focus stays inside while it is open.
EscapeCloses the popover and returns focus to the trigger. With a Select, Menu or Tooltip open inside it, Escape closes that one first.

Source: react-aria-components 1.22.1 DialogTrigger, Popover and Dialog (react-aria 3.53.1 usePopover, which contains focus while open) through _engines/react-aria/popover.client.tsx. The first-control rule and focus return are Nizivo's (ARCHITECTURE.md §12.2), checked by the overlay lane (test/overlay/popover.spec.tsx, nesting-popover-drawer.spec.tsx) in Chromium, Firefox and WebKit; apps/docs/scripts/smoke.ts checks opening with Enter, Escape closing and focus back on the trigger.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

  • Canonical: none
  • Ghost masters: none

On this page