Experience System
Components

Popover

A floating panel anchored to a trigger, opened by click—compact forms, confirmations, and contextual actions.

Use it for secondary forms, filters, or actions that should stay in context without navigating away. It uses Blue Yonder surface styling and portals through ThemeProvider so the panel renders in the correct container.

Unlike HoverCard, which reveals content on hover, Popover is intended for explicit open/close interaction and can host interactive controls such as Input, Button, or Label.

Installation

The component is exported from @by/experience-system. Add the package with your package manager:

pnpm add @by/experience-system

In this monorepo, depend on the workspace package (for example via workspace:* or your catalog) so imports resolve to packages/experience-system.

Composition

Use the following composition to build a Popover:

Popover
├── PopoverTrigger
├── PopoverAnchor (optional)
└── PopoverContent

PopoverContent already wraps the panel in a portal that targets the ThemeProvider container, so you typically render it directly without a separate portal. Behavior follows Radix Popover.

Usage

import {
  Button,
  Input,
  Label,
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@by/experience-system';

Popover and its parts are client components ('use client'). Wrap your tree in ThemeProvider so PopoverContent can resolve the portal container. Use them inside a Client Component or a dynamic import when using the Next.js App Router.

<Popover>
  <PopoverTrigger asChild>
    <Button>Open popover</Button>
  </PopoverTrigger>
  <PopoverContent className="w-80">
    <div className="grid gap-4">
      <div className="space-y-2">
        <h4 className="text-lg leading-none font-medium">Dimensions</h4>
        <p className="text-sm text-neutral-11">Set the dimensions for the layer.</p>
      </div>
      <div className="grid grid-cols-3 items-center gap-4">
        <Label htmlFor="width">Width</Label>
        <Input id="width" defaultValue="100%" className="col-span-2 h-8" />
      </div>
    </div>
  </PopoverContent>
</Popover>

When to use

  • Compact tasks next to a control (dimensions, filters, quick edits) where a full-page or Dialog would feel heavy.
  • Optional detail that should not clutter the default layout until the user asks for it.
  • Touch and keyboard flows where hover is not reliable—popover uses explicit activation like DropdownMenu, not pointer hover.

When not to use

  • Blocking or critical flows that must capture full attention—prefer Dialog or Drawer with clear primary actions.
  • Hover-only previews—use HoverCard or Tooltip when the content is read-only and hover is acceptable for your audience.
  • Long scrolling forms or dense multi-step content—use a dedicated page or larger overlay surface.

API Reference

Pieces map to Radix UI Popover primitives. The tables below mirror the upstream API in full; styling, motion, and the portal container come from @by/experience-system and ThemeProvider.

Popover

Popover contains all the parts of a popover. Exported as Popover; forwards Popover.Root props from radix-ui.

PropTypeDefault
defaultOpenbooleanNo default value
openbooleanNo default value
onOpenChangefunctionNo default value
modalbooleanfalse

PopoverTrigger

PopoverTrigger — the control that toggles the popover. Use asChild to merge props into a Button or other focusable element.

PropTypeDefault
asChildbooleanfalse
Data attributeValues
[data-state]"open" | "closed"

PopoverAnchor

PopoverAnchor — optional element to position the panel against. Use it when alignment should reference something other than the trigger (for example, anchoring to a row or input while the trigger lives elsewhere).

PropTypeDefault
asChildbooleanfalse
virtualRefReact.RefObject<MeasurableElement>No default value

PopoverContent

PopoverContent — the floating panel that opens from the trigger. This export wraps Popover.Portal with the Experience System container from useContainerElement(), so you typically render PopoverContent directly without a separate PopoverPortal. Default classes provide border, shadow, padding, and enter/exit motion.

PropTypeDefault
asChildbooleanfalse
onOpenAutoFocusfunctionNo default value
onCloseAutoFocusfunctionNo default value
onEscapeKeyDownfunctionNo default value
onPointerDownOutsidefunctionNo default value
onFocusOutsidefunctionNo default value
onInteractOutsidefunctionNo default value
forceMountbooleanNo default value
sideenum"bottom"
sideOffsetnumber8 (experience system override of Radix 0)
alignenum"center" (Experience System default)
alignOffsetnumber0
avoidCollisionsbooleantrue
collisionBoundaryBoundary[]
collisionPaddingnumber | Padding0
arrowPaddingnumber0
stickyenum"partial"
hideWhenDetachedbooleanfalse
Data attributeValues
[data-state]"open" | "closed"
[data-side]"left" | "right" | "bottom" | "top"
[data-align]"start" | "end" | "center"
CSS variableDescription
--radix-popover-content-transform-originThe transform-origin computed from the content and trigger positions
--radix-popover-content-available-widthThe remaining width between the trigger and the boundary edge
--radix-popover-content-available-heightThe remaining height between the trigger and the boundary edge
--radix-popover-trigger-widthThe width of the trigger
--radix-popover-trigger-heightThe height of the trigger

@by/experience-system does not currently re-export PopoverPortal, PopoverClose, or PopoverArrow. If you need them, import them from radix-ui directly; for the full primitive reference and any upstream updates, see the Radix docs.

Accessibility

Use a real control for PopoverTrigger (for example Button or Link) so keyboard users can focus it and open the panel per Radix — accessibility. Keep focus order predictable inside PopoverContent when it contains fields or actions, and provide an explicit dismiss path (close button, primary action, or onOpenChange) when the panel is non-modal.

Keyboard interactions

KeyDescription
Space / EnterOpens or closes the popover when focus is on PopoverTrigger.
TabMoves focus to the next focusable element inside PopoverContent (and out of the panel after the last one when modal={false}).
Shift + TabMoves focus to the previous focusable element.
EscCloses the popover and returns focus to PopoverTrigger.

Source in the repo: packages/experience-system/src/components/Popover/Popover.tsx. Agent-oriented contracts: packages/experience-system/src/components/Popover/Popover.instructions.md.