Popover
A floating panel anchored to a trigger for arbitrary controls and content.
Show codeHide code
Installation
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
PopoverTitle,
PopoverDescription,
PopoverClose,
} from "@makeplane/propel/components/popover";Usage
A popover is a trigger-anchored panel for arbitrary content — a form, a property sheet, or a short
explanation — not a menu. Compose PopoverTrigger onto any control via render, then put the
panel in PopoverContent. Tall rich stacks go in PopoverBody. The panel positions relative to
the trigger that opened it.
import { Button } from "@makeplane/propel/components/button";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@makeplane/propel/components/popover";
export default function UsageDemo() {
return (
<Popover>
<Button
stretch="auto"
variant="secondary"
size="lg"
render={<PopoverTrigger />}
label="About"
/>
<PopoverContent variant="text">
<PopoverTitle>Heading text</PopoverTitle>
<PopoverDescription>
A popup that displays information related to an element.
</PopoverDescription>
</PopoverContent>
</Popover>
);
}Anatomy
| Part | Role |
|---|---|
Popover |
Root context (open state, modality, handle, payload) |
PopoverTrigger / PopoverClose |
Behavior grafts onto Button / IconButton via render. Trigger: click, click+hover (openOnHover), or hover only (openOnHover + openOnClick={false}) |
PopoverContent |
Portal + positioner + popup. variant is rich (default) or text |
PopoverBody |
Scrollable slot for tall rich content. Title and actions stay outside so they stay pinned |
PopoverTitle / PopoverDescription |
Accessible name + supporting copy (especially on variant="text") |
rich is a 16px-padded, 12px-radius card with a 320px minimum. It grows with wider content; wrap
children only to force a different width. The panel caps at the height available next to the
trigger — put forms and long stacks in PopoverBody so they scroll; keep PopoverTitle and
actions as siblings. When the body can overflow, pass tabIndex={0}. text is a compact 296px
card with tight title + description padding.
Positioning lives on PopoverContent: side (default bottom), align (default start), plus
the usual offset, collision, and anchor props. Pass a different trigger through PopoverTrigger’s
render; the panel always anchors to the control that opened it.
Examples
Text
variant="text" is the compact title + description card. PopoverTitle and PopoverDescription
wire aria-labelledby / aria-describedby on the panel.
Show codeHide code
Open on hover
openOnHover (with a tuned delay) opens the panel on hover in addition to click. Keyboard and
touch still work.
Show codeHide code
Hover only
Pass openOnHover with openOnClick={false} when a mouse click should not toggle the panel —
glanceable previews, for example. Keyboard (Enter/Space) and touch still open so the surface is not
pointer-locked.
Show codeHide code
Controlled
External state drives the popover via open and onOpenChange, so any control can open or close
the panel.
Show codeHide code
Detached trigger
createPopoverHandle() links a trigger that lives outside the Popover to it via the handle
prop, so a button can open a panel declared elsewhere in the tree. The panel still anchors to the
trigger that opened it.
Show codeHide code
Accessibility
The popup is role="dialog". Name it with PopoverTitle (preferred) or aria-label on
PopoverContent. PopoverDescription becomes the described-by text. The trigger exposes
aria-expanded / aria-haspopup from Base UI.
Keyboard: Enter / Space on the trigger opens the panel; Escape or an outside click dismisses
it and returns focus to the trigger. PopoverClose (typically a Cancel/Save Button) also
dismisses. openOnHover still leaves click available; openOnClick={false} suppresses only mouse
click — keyboard and touch still open.
Default modal={false} leaves the page interactive and does not trap focus. modal={true} locks
page scroll and outside pointers; modal="trap-focus" keeps focus in the panel without locking
the page. Either trapping mode needs PopoverClose inside the popup so touch screen readers can
leave. There is no backdrop — the panel does not dim the page.
API Reference
Popover
PopoverTrigger
The element that opens the popover. Renders a `<button>` by default; pass `render` to project the trigger onto any control (`Button`, `IconButton`, a custom element). The panel anchors to whichever trigger opened it. Interaction: click only (default), click + hover (`openOnHover`), or hover only (`openOnHover` and `openOnClick={false}`).
PopoverContent
A trigger-anchored content panel. Composes the popover portal + positioner + styled popup so a consumer only writes the trigger and the panel body. Pass any trigger through `PopoverTrigger`'s `render`; pass `variant="text"` for the compact title + description card. Put tall `rich` content in `PopoverBody` so it scrolls under the available-height cap.
PopoverBody
The scrollable slot inside `PopoverContent`. Grows to fill leftover space under the popup's available-height cap and scrolls when the body is taller than that space. Put forms and long stacks here; keep `PopoverTitle` and action rows as siblings so they stay pinned. When the body can overflow, pass `tabIndex={0}` so the scroll region is keyboard-reachable (axe `scrollable-region-focusable`).
PopoverTitle
The accessible title for the popover: Base UI's `Popover.Title` behavior (links to the popup via `aria-labelledby`) grafted onto the family-owned title chrome (`text-body-sm-medium`).
PopoverDescription
Supporting text for the popover: Base UI's `Popover.Description` behavior (wired as the popup's `aria-describedby` target) grafted onto the shared `internal/OverlayDescription` at `md` (body-xs secondary).
PopoverClose
The behavior that closes the popover when activated. Use as the `render` target of a `Button` or `IconButton` so the styled primitive's look wins via render-composition: ```tsx <Button variant="secondary" size="sm" stretch="auto" label="Cancel" render={<PopoverClose />} />; ``` Maps 1:1 to `Popover.Close`.