# Popover

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/popover
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=384-2

```tsx
import { useState } from 'react';

import { Button } from '@commenda/pathfinder/button';
import { Input } from '@commenda/pathfinder/input';
import { Popover } from '@commenda/pathfinder/popover';

export const PopoverDefault = () => {
  const [open, setOpen] = useState(false);

  return (
    <Popover
      title="Publish workflow"
      trigger={<Button variant="secondary">Publish workflow</Button>}
      open={open}
      onOpenChange={setOpen}
      footerConfig={{
        primaryAction: {
          label: 'Publish',
          type: 'submit',
          form: 'publish-workflow',
        },
      }}
    >
      <form
        id="publish-workflow"
        onSubmit={(event) => {
          event.preventDefault();
          setOpen(false);
        }}
      >
        <Input label="Version name" defaultValue="Version 2" />
      </form>
    </Popover>
  );
};
```


Import `Popover` and optional `PopoverGroup` from `@commenda/pathfinder/popover`.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `mode` | `"form" \| "preview"` | conditional | form; click versus hover/focus |
| `trigger` | `ReactElement<any, string \| React.JSXElementConstructor<any>>` | required | Required; forward native props/ref |
| `title` | `string` | required | Required accessible heading |
| `description` | `ReactNode` | optional | Supporting text and content |
| `children` | `ReactNode` | required | Supporting text and content |
| `positionConfig` | `{ side?: "top" \| "right" \| "bottom" \| "left"; align?: "start" \| "center" \| "end"; }` | optional | bottom / start, collision-aware |
| `open` | `boolean` | conditional | Application can control opening |
| `defaultOpen` | `boolean` | optional | Application can control opening |
| `onOpenChange` | `((open: boolean) => void)` | conditional | Application can control opening |
| `footerConfig` | `OverlayFooterConfig` | optional | Form only |
| `focusConfig` | `OverlayFocusConfig` | optional | Form only |
| `loadingConfig` | `{ isLoading: boolean; label?: string; }` | optional | Preview only |
| `className` | `string` | optional | Popup classes |

### positionConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | No |  |
| `align` | `"start" \| "end" \| "center"` | No |  |

### footerConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `primaryAction` | `OverlayAction` | No |  |
| `secondaryAction` | `OverlayAction` | No |  |
| `leadingContent` | `ReactNode` | No |  |
| `cancelLabel` | `string` | No |  |

### focusConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `initialFocusRef` | `RefObject<HTMLElement \| null>` | No |  |
| `returnFocusRef` | `RefObject<HTMLElement \| null>` | No |  |

### loadingConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isLoading` | `boolean` | Yes |  |
| `label` | `string` | No |  |

Forms use Base UI Popover; previews use PreviewCard. Width is 320px, clamped to the viewport; offset is 4px, radius 10px. Use Dialog or Sheet for longer tasks. Form footer actions have the same shape as [Dialog](/docs/components/dialog), including native form submission. Cancel closes; primary actions stay open until the application closes them.

## Preview recipes [#preview-recipes]


```tsx
import { Avatar } from '@commenda/pathfinder/avatar';
import { Badge } from '@commenda/pathfinder/badge';
import { DescriptionList } from '@commenda/pathfinder/description-list';
import { USFlag } from '@commenda/pathfinder/flags/USFlag';
import { Popover, PopoverGroup } from '@commenda/pathfinder/popover';

export const PopoverPreview = () => (
  <PopoverGroup>
    <div className="pf:flex pf:flex-wrap pf:gap-xl">
      <Popover
        mode="preview"
        title="Acme Inc."
        trigger={<a href="#entity-record">Acme Inc.</a>}
      >
        <DescriptionList
          size="compact"
          layout="stacked"
          items={[
            {
              id: 'country',
              label: 'Country',
              value: 'United States',
              leadingIcon: <USFlag size={16} />,
            },
            {
              id: 'status',
              label: 'Status',
              value: <Badge label="Active" tone="success" />,
            },
          ]}
        />
      </Popover>
      <Popover
        mode="preview"
        title="Annual report"
        trigger={<a href="#filing-record">Annual report</a>}
      >
        <DescriptionList
          size="compact"
          layout="stacked"
          items={[
            { id: 'entity', label: 'Entity', value: 'Acme Inc.' },
            { id: 'due', label: 'Due date', value: '1 March 2027' },
          ]}
        />
      </Popover>
      <Popover
        mode="preview"
        title="Certificate of incorporation.pdf"
        trigger={<a href="#file-record">Certificate</a>}
      >
        <p>PDF · 248 KB</p>
        <p>Uploaded 12 October 2026</p>
      </Popover>
      <Popover
        mode="preview"
        title="Avery Morgan"
        trigger={<a href="#person-record">Avery Morgan</a>}
      >
        <Avatar
          identity={{ kind: 'person', name: 'Avery Morgan', userId: 'avery' }}
        />
        <p>avery@example.com</p>
        <a href="mailto:avery@example.com">Email Avery</a>
      </Popover>
    </div>
  </PopoverGroup>
);
```


Entity, record, file, and person previews compose existing components. Keep them read-only. The trigger stays a real link and touch activation follows its destination. General previews allow one destination; the Figma person recipe permits up to two. Preview hover open and close delays are 150ms; keyboard focus also opens the preview without moving focus into it.

## Loading [#loading]


```tsx
import { Popover } from '@commenda/pathfinder/popover';

export const PopoverLoading = () => (
  <Popover
    mode="preview"
    title="Entity preview"
    trigger={<a href="#loading-record">Hover for loading preview</a>}
    loadingConfig={{ isLoading: true }}
  >
    Entity details
  </Popover>
);
```


Applications own fetching, cancellation, errors, and cached data. Skeletons match the content area and announce the loading state.

Dialog and Sheet provide a local scope automatically. Wrap page siblings with `PopoverGroup` to ensure opening one closes another; standalone popovers remain independent. Controlled callers must honor `onOpenChange(false)`. There is no global singleton or required app provider. Read the [overlay rules](/docs/components/popover#design-rules).

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Popover [#popover]

- **POPOVER-001:** Width320, radius10, menu shadow, 0.5px pearl300 edge, offset4, no backdrop. Form header16px start/8px end, top12/bottom4; content16px horizontal, footer12px vertical. Forms contain one or two fields; use larger surfaces for longer tasks.
- **POPOVER-002:** Read-only previews open/close after150ms hover delays and support keyboard focus. Keep trigger destination on touch. No edit controls in hover previews. Person permits two links as an explicit exception to the general one-link recommendation.
- **POPOVER-003:** Coordinate within the nearest local overlay/PopoverGroup. Fetching, caching, loading, permissions and application state remain outside the component.

### Motion and edge cases [#motion-and-edge-cases]

Occasional overlays use motion for spatial consistency. Motion paths and timings follow Kakashi: Dialog fades and moves from 95% scale with a 2% vertical offset in 200ms both ways; Popover fades from 95% scale and an 8px offset toward its trigger in 150ms both ways. Popover direction follows the resolved placement after collision handling. Right Sheet slides in for 500ms and out for 300ms; bottom Sheet slides for 500ms both ways. Sheet panels do not fade during normal motion, and modal backdrops follow the panel duration.

Pathfinder retains its foundation ease-out curve for Dialog/Popover and drawer curve `(0.32,0.72,0,1)` for Sheets. These are deliberate curve choices; this is not a copy of Kakashi’s legacy Radix keyframe implementation. Base UI continues to own presence, focus and dismissal. Transitions use a single `transform` value plus opacity where relevant, so Tailwind v4’s separate scale/translate properties cannot bypass interpolation. Closing or reopening mid-transition reverses from the current rendered position.

Keyboard opening and dismissal remain immediate, including Close and Cancel activation. Reduced motion removes scale and translation: Dialog/Popover keep their fade, while Sheets change position immediately and their modal backdrop can fade. Drag writes only the popup transform, never layout or inherited animation variables. Programmatic openings animate by default; explicit keyboard interactions suppress motion.
