FairGarden Design

Popover

Non-modal content anchored to a trigger: a small details panel, a definition, or the touch stand-in for a tooltip.

import {
  Popover,
  PopoverArrow,
  PopoverClose,
  PopoverDescription,
  PopoverPopup,
  PopoverSource,
  PopoverTitle,
  PopoverTrigger,
} from '@fairgarden-private/design/components/Popover'

The panel renders in a portal as a white scope that follows the page mode, whatever the trigger's ground. Its --border-size-2 --primary12 frame carries the boundary in both modes, with --ds-radius-8 and no shadow. It sits 8 px from the trigger, aligned to its start edge, and flips before it would clip. It is 240 to 360 px wide from 768 px, and the viewport less its margins below. It opens on press, never on hover, instantly or with a clip reveal from the trigger side; Esc or an outside press closes it. Popovers are hidden in print.

Keep essential instructions out of a popover. A filter panel with more than two controls, or anything that needs confirmation, belongs in a Dialog.

Panel and definition

Panel and definition

kind="panel" (the default) is a padded panel with a title in type-subhead; put PopoverClose first so the title wraps beside it. kind="definition" sets the term in tracked caps, the definition in type-body-ui and an optional PopoverSource caption. PopoverArrow adds the tail, whose edge continues the frame.

PopoverKinds.tsx
'use client'

import {
  Popover,
  PopoverArrow,
  PopoverClose,
  PopoverDescription,
  PopoverPopup,
  PopoverSource,
  PopoverTitle,
  PopoverTrigger,
} from '@fairgarden-private/design/components/Popover'
import styles from './kinds.module.css'

export function PopoverKinds() {
  return (
    <div className={styles.row}>
      <Popover>
        <PopoverTrigger variant="outline">Trail Details</PopoverTrigger>
        <PopoverPopup>
          <PopoverArrow />
          <PopoverClose />
          <PopoverTitle>Ridge Loop</PopoverTitle>
          <PopoverDescription>
            6.4 km with 310 m of climbing. Open dawn to dusk; dogs stay on a leash.
          </PopoverDescription>
        </PopoverPopup>
      </Popover>

      <Popover>
        <PopoverTrigger variant="text" icon="help" iconPosition="end">
          Riparian Buffer
        </PopoverTrigger>
        <PopoverPopup kind="definition">
          <PopoverArrow />
          <PopoverTitle>Riparian buffer</PopoverTitle>
          <PopoverDescription>
            The strip of native plants along a stream that filters runoff and shades the water.
          </PopoverDescription>
          <PopoverSource>State Watershed Guide, 2024</PopoverSource>
        </PopoverPopup>
      </Popover>
    </div>
  )
}

API Reference

Popover and PopoverTrigger

Groups the parts of a popover (Base UI Popover.Root). Esc or an outside press closes it.

Opens the popover on press, never on hover (§10.15). Renders a Button (§9.2); its data-popup-open shows the expanded state.

PropTypeDescription
butted
'start' | 'end' | null | undefined

Butts the button against an adjacent field on its start or end edge, as in the butted submit. Default: none.

handle
PopoverHandle<unknown> | undefined

Associates a detached trigger with a Popover created by Popover.createHandle.

icon
| 'menu'
| 'search'
| 'circle'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'chevron_right'
| 'chevron_left'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail'
| undefined

One optional functional glyph (§6.10), inline tier, FILL 0.

iconOnly
boolean | null | undefined

true hides the label and shows only icon. Default false.

iconPosition
'start' | 'end' | undefined

Which side of the label the glyph sits on (a text Button’s chevron trails).

onMedia
false | boolean | null | undefined

Only with iconOnly.

onPress
PressCallback | undefined

Called on click, after onClick.

payload
unknown | undefined

A payload handed to the Popover’s children function when this trigger opens it.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale, from the primary roster: the outline edge, labels and focus ring. Never defaulted; omitted, it inherits the scope [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale: the solid fill and the text glyph. Omitted, solid falls back to the scope’s action scale.

size
'sm' | 'md' | 'lg' | 'xl' | null | undefined

Fixed height: sm 32 px (hit area extended to 44), md 40 px (default), lg 48 px, xl 56 px for the page’s single transactional action.

variant
'text' | 'solid' | 'outline' | null | undefined

outline (default) is the outline twin; solid is the page’s primary action, filled with the scope’s action scale; text is a text button whose glyph trails (§9.2).

children
React.ReactNode | undefined

The label: verb plus object, authored in title case [D160].

PopoverPopup and PopoverArrow

The anchored panel, rendered in its Base UI Portal as a nested white scope: --primary1 face, --border-size-2 --primary12 frame, --ds-radius-8, 240–360 px wide from --md-n-above and the viewport less its margins below. It opens instantly or with a clip reveal from the trigger side. Render PopoverArrow inside it for the tail.

PropTypeDescription
kind
'panel' | 'definition' | null | undefined

panel (default): a padded panel. definition: a term, its definition and an optional source.

primary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale inside the popup’s white scope. Omitted, the white preset’s default [D133].

secondary
| 'ruby'
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null
| undefined

Secondary Radix scale inside the popup: link underlines and other accents only.

container
| HTMLElement
| ShadowRoot
| React.RefObject<HTMLElement | ShadowRoot | null>
| null
| undefined

The element the portal renders into. Default: document.body.

align
Align | undefined

Alignment to the trigger. Default start.

alignOffset
number | OffsetFunction | undefined

Offset along the alignment axis in px. Default 0.

side
Side | undefined

Which side of the trigger the panel sits on. Default bottom; it flips when there is no room.

sideOffset
number | OffsetFunction | undefined

Distance from the trigger in px. Default 8 (--size-px-2).

collisionPadding
Padding | undefined

Clearance from the viewport edge in px before the panel shifts or flips. Default 16.

keepMounted
boolean | undefined

Keeps the portal mounted while closed.

The tail: a --ds-space-12 × 6 px triangle filled with the face, its --border-size-2 --primary12 edge continuing the panel’s frame.

PopoverTitle, PopoverDescription and PopoverSource

The popover’s heading (Base UI Popover.Title, an h2) and accessible name. In the definition kind it is the term, in tracked caps (type-label); in a panel, type-subhead.

The content or the definition (Base UI Popover.Description), in type-body-ui --primary12.

A definition’s source caption, in type-caption --role-muted.

PopoverClose

The optional close X, top end: an icon-only Button with the inline-tier close glyph and a --ds-size-hit target (§9.2). Pass render to close from another control instead; label is then ignored.

PropTypeDescription
label
string | undefined

The close X’s accessible name, visually hidden. Default “Close”.

Additional types

popover
PopoverArrowProps

Props for PopoverArrow: Base UI Popover.Arrow props.

type PopoverArrowProps = PopoverArrow.PopoverArrowProps
PopoverCloseProps

Props for PopoverClose: Base UI Popover.Close props plus the X’s accessible name.

type PopoverCloseProps = PopoverCloseProps & { label?: string }
PopoverDescriptionProps

Props for PopoverDescription: Base UI Popover.Description props.

type PopoverDescriptionProps = PopoverDescription.PopoverDescriptionProps
PopoverKind
type PopoverKind = 'panel' | 'definition'<'panel' | 'definition' | null | undefined>
PopoverPopupProps

Props for PopoverPopup: Base UI Popover.Popup props plus placement, kind, color axes and portal options.

type PopoverPopupProps = PopoverPopupProps & {
  kind?: 'panel' | 'definition' | null;
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  side?: Side;
  align?: Align;
  sideOffset?: number | OffsetFunction;
  alignOffset?: number | OffsetFunction;
  collisionPadding?: Padding;
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null;
  keepMounted?: boolean;
}
PopoverProps

Props for Popover: Base UI Popover.Root props (open, onOpenChange, modal …).

type PopoverProps<Payload = unknown> = PopoverRoot.Props<Payload>
PopoverSourceProps

Props for PopoverSource: p props and render.

type PopoverSourceProps = useRender.ComponentProps<'p'>
PopoverTitleProps

Props for PopoverTitle: Base UI Popover.Title props.

type PopoverTitleProps = PopoverTitle.PopoverTitleProps
PopoverTriggerProps

Props for PopoverTrigger: Button props plus Base UI’s trigger options.

type PopoverTriggerProps = (
  | {
      iconOnly?: false | null;
      onMedia?: false | null;
      size?: 'sm' | 'md' | 'lg' | 'xl' | null;
      icon?:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      children?: React.ReactNode;
    }
  | {
      iconOnly: true;
      onMedia?: boolean | null;
      size?: 'sm' | 'md' | 'lg' | null;
      icon:
        | 'menu'
        | 'search'
        | 'circle'
        | 'arrow_forward'
        | 'arrow_upward'
        | 'expand_more'
        | 'close'
        | 'remove'
        | 'add'
        | 'check'
        | 'chevron_right'
        | 'chevron_left'
        | 'more_horiz'
        | 'play_arrow'
        | 'pause'
        | 'download'
        | 'zoom_in'
        | 'zoom_out'
        | 'recenter'
        | 'help'
        | 'mail';
      children: React.ReactNode;
    }
) & {
  /**
   * `outline` (default) is the outline twin; `solid` is the page's primary
   * action, filled with the scope's action scale; `text` is a text button
   * whose glyph trails (§9.2).
   */
  variant?: 'text' | 'solid' | 'outline' | null;
  /**
   * Butts the button against an adjacent field on its `start` or `end`
   * edge, as in the butted submit. Default: none.
   */
  butted?: 'start' | 'end' | null;
  /**
   * Primary Radix scale, from the primary roster: the outline edge, labels
   * and focus ring. Never defaulted; omitted, it inherits the scope [D133].
   */
  primary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | null;
  /**
   * Secondary Radix scale: the `solid` fill and the `text` glyph. Omitted,
   * `solid` falls back to the scope's action scale.
   */
  secondary?:
    | 'ruby'
    | 'olive'
    | 'sage'
    | 'slate'
    | 'sand'
    | 'gray'
    | 'mauve'
    | 'brown'
    | 'bronze'
    | 'gold'
    | 'red'
    | 'crimson'
    | 'tomato'
    | 'pink'
    | 'plum'
    | 'indigo'
    | 'iris'
    | 'violet'
    | 'purple'
    | 'amber'
    | 'blue'
    | 'cyan'
    | 'grass'
    | 'green'
    | 'jade'
    | 'lime'
    | 'mint'
    | 'orange'
    | 'sky'
    | 'teal'
    | 'yellow'
    | null;
  /** Which side of the label the glyph sits on (a `text` Button's chevron trails). */
  iconPosition?: 'start' | 'end';
  /**
   * Called on click, after `onClick`.
   * @deprecated Use `onClick`. Kept as an alias from the react-aria Button.
   */
  onPress?: PressCallback;
  /** Associates a detached trigger with a Popover created by `Popover.createHandle`. */
  handle?: PopoverHandle<unknown>;
  /** A payload handed to the Popover's children function when this trigger opens it. */
  payload?: unknown;
}

Specification: DESIGN-SYSTEM.md §10.15 (popover) and §10.1 (the overlay surface).