Skip to content
Beta · ExperimentalReact 19No peer dependencies

Popover

A panel anchored to the control that opens it, in the top layer, with light dismiss, Escape and focus returned to the trigger.

Beta components are subject to change and may break your code. Use them at your own risk, and share feedback through the bug tracker.

On this page

Installation

bash
npx gbs-add-block@latest -a Popover -beta

The block copies the popover folder into your project, along with the small shared folder that every component imports. You own the code and can change it freely. There are no peer dependencies other than React.

Requirements

  • React 19 and @types/react 19
  • TypeScript target ES2022 or newer, with "jsx": "react-jsx"
  • Browsers from 2024 or newer (the styles use light-dark() and the Popover API)

Import the stylesheet once, for example in your global CSS:

css
@import "../components/popover/styles.css";

or in your root layout / entry file:

ts
import "@/components/popover/styles.css";

A popover is a small panel attached to the control that opened it: a filter form, a colour picker, a paragraph of help too long for a tooltip. It renders in the browser's top layer, so it is never clipped by a scrolling parent, and the browser handles clicking away and Escape.

Set the --gbs-* variables on :root to theme every component at once, the grid included. Each component's own variables fall back to them, and then to the built-in palette, so components look identical out of the box.

Default

Live preview

Quick Start

tsx
import { Popover } from "@/components/popover";
import "@/components/popover/styles.css";
 
<Popover trigger={<Button variant="outline">Filters</Button>} title="Filters">
  {({ close }) => (
    <>
      <Checkbox label="Only active" />
      <Checkbox label="Has attachments" />
      <Button size="sm" data-autofocus onClick={close}>Apply</Button>
    </>
  )}
</Popover>

children may be a function, which receives { close } — handy for a button that applies and dismisses in one go.

Props Table

PropTypeDefaultDescription
triggerReactElementThe element that opens it. Cloned with the ARIA wiring.
childrenReactNode | ({ close }) => ReactNodeThe panel's contents.
openbooleanControlled open state.
defaultOpenbooleanfalseUncontrolled initial state.
onOpenChange(open, reason?) => voidreason is escape, outside, trigger or api.
titleReactNodeHeading inside the panel, which also names it.
descriptionReactNodeA line under the title.
aria-labelstring"More information"Names the panel when there is no title.
side"top" | "bottom" | "left" | "right"bottomSide to open on, before flipping for space.
align"start" | "center" | "end"startWhich edge to line up with.
gapnumber6Distance from the trigger.
scrollablebooleanfalseCap the height to the space available and scroll.
autoFocusbooleantrueMove focus into the panel on open.
id, className, classNames, style, localeTextSlots below.
refRef<PopoverHandle>{ open, close, getElement }.

Focus

When it opens, focus moves to the first element marked data-autofocus, or to the panel itself if there is none. When it closes, focus goes back to the trigger, so keyboard work carries on where it left off.

tsx
<Popover trigger={<Button>Rename</Button>} title="Rename">
  {({ close }) => (
    <>
      <Input label="Name" data-autofocus />
      <Button onClick={close}>Save</Button>
    </>
  )}
</Popover>

A popover does not trap focus, and it should not: Tab moves through the panel and on into the page, and the browser's light dismiss closes it when focus or a click lands elsewhere. If your content needs focus held — a form that must be finished or cancelled — use the Modal.

Which one to use

You wantUse
A list of commandsMenu
A few words describing a controlTooltip
A small panel of content or controlsPopover
Something that must be finished or dismissedModal or Dialog

Styling and Theming

All rules are in the CSS components layer, so utility classes passed through className / classNames override them.

tsx
<Popover trigger={<Button>Filters</Button>} className="w-72" classNames={{ body: "gap-3" }}>

CSS variables

VariableUsed for
--pv-bg, --pv-fgPanel background and text.
--pv-mutedThe description.
--pv-border, --pv-shadow, --pv-radiusPanel shape.
--pv-focusFocus ring when the panel itself is focused by keyboard.
--pv-font-sizeBase size.
--pv-pxPadding (default 12px).
--pv-widthMaximum width (default 280px).
--pv-durationEntry animation length.

Override them on .pv-root, on :root, or through style. Each variable falls back to the shared --gbs-* of the same name, then to the DataGrid's --dg-* when that stylesheet is loaded, and finally to the built-in palette.

Data attributes

ElementAttributes
Root (.pv-root)data-side — the side it settled on, which the entry animation follows.

Next.js

The file is already marked "use client". Import the stylesheet in your root layout and render the Popover inside a client component.

Notes

  • Rendered in the top layer through the native Popover API, so no portal and no z-index battles.
  • Placement comes from shared/core/position.ts, which is unit-tested on its own.
  • Nothing is measured or mounted until it opens.