Skip to main content

Popover

A flexible, dependency‑free popover built for @libdev‑ui. It positions a floating card relative to an anchor (or the trigger when no anchor is provided), renders through a portal to document.body, and supports the standard 12 placements (top, top-start, top-end, right, right-start, right-end, bottom, bottom-start, bottom-end, left, left-start, left-end).

The popover manages focus, outside clicks, and Escape key by default. The arrow is drawn with pure CSS and is clamped away from rounded corners for a clean look.


Import

import {
PopoverForm,
PopoverTrigger,
PopoverAnchor,
PopoverCard,
PopoverClose,
PopoverArrow,
} from "@libdev-ui/base";

Demo

click any chip
import React from "react";
import {
PopoverForm,
PopoverTrigger,
PopoverAnchor,
PopoverCard,
PopoverClose,
PopoverArrow,
Button,
Text,
Box,
} from "@libdev-ui/base";

export default function BasicPopover(): JSX.Element {
return (
<PopoverForm placement="bottom">
<PopoverTrigger>
<Button>Open</Button>
</PopoverTrigger>

{/* Ако липсва Anchor, за референтен елемент се използва Trigger-ът */}
<PopoverAnchor />

<PopoverCard style={{ padding: 16, maxWidth: 320 }}>
<Box
sl={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 8,
}}
>
<strong>Title</strong>
<PopoverClose></PopoverClose>
</Box>

<Text style={{ marginBottom: 12 }}>Any content goes here.</Text>

<Button variant="soft" color="primary">
Action
</Button>
<PopoverArrow size={8} />
</PopoverCard>
</PopoverForm>
);
}

Usage

<PopoverForm>
<PopoverTrigger></PopoverTrigger>
<PopoverAnchor /> {/* optional; falls back to Trigger if omitted */}
<PopoverCard>
…content…
<PopoverClose></PopoverClose>
<PopoverArrow />
</PopoverCard>
</PopoverForm>

Quick start

import {
PopoverForm,
PopoverTrigger,
PopoverAnchor,
PopoverCard,
PopoverClose,
PopoverArrow,
Button,
Text,
Box,
} from "@libdev-ui/base";

export default function BasicPopover() {
return (
<PopoverForm placement="bottom">
<PopoverTrigger>
<Button>Open</Button>
</PopoverTrigger>

{/* Ако липсва Anchor, за референтен елемент се използва Trigger-ът */}
<PopoverAnchor />

<PopoverCard style={{ padding: 16, maxWidth: 320 }}>
<Box
sl={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 8,
}}
>
<strong>Title</strong>
<PopoverClose></PopoverClose>
</Box>

<Text style={{ marginBottom: 12 }}>Any content goes here.</Text>

<Button variant="soft" color="primary">
Action
</Button>
<PopoverArrow size={8} />
</PopoverCard>
</PopoverForm>
);
}

Controlled vs uncontrolled

  • Uncontrolled: omit open; use defaultOpen and let the component manage its own state.
  • Controlled: pass open and onOpenChange to fully control visibility.
function Controlled() {
const [open, setOpen] = React.useState(false);
return (
<PopoverForm open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<Button onClick={() => setOpen((o) => !o)}>
{open ? "Close" : "Open"}
</Button>
</PopoverTrigger>
<PopoverAnchor />
<PopoverCard style={{ padding: 12 }}>
controlled content
<PopoverClose></PopoverClose>
<PopoverArrow />
</PopoverCard>
</PopoverForm>
);
}

Placement

placement accepts the following values:

top | top-start | top-end |
right | right-start | right-end |
bottom | bottom-start | bottom-end |
left | left-start | left-end
  • *-start and *-end align the card along the secondary axis.
  • The card is clamped within the viewport with a small padding.
  • The arrow is automatically clamped away from rounded corners and has a small guard near the extremes, so it never hugs the very edge.

Accessibility

  • The trigger receives aria-haspopup="dialog" and manages aria-expanded while open.
  • The card uses role="dialog" by default (can be changed via the role prop).
  • Escape closes the popover (unless disableOutsideClose is true).
  • Focus returns to the trigger when the popover closes.

API

<PopoverForm />

Container that provides context, state, positioning and event wiring.

PropTypeDefaultDescription
openbooleanControls visibility (controlled mode).
defaultOpenbooleanfalseInitial visibility (uncontrolled mode).
onOpenChange(open: boolean) => voidCalled when open state changes.
placement'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end''bottom'Where to place the card relative to the anchor/trigger.
offsetnumber8Gap between the card and the anchor. Arrow size is handled separately in CSS.
disableOutsideClosebooleanfalseIf true, disables closing via outside click & Escape.
childrenReactNodeComposition children (Trigger, Anchor, Card, etc.).

Behavior notes

  • If <PopoverAnchor /> is not rendered or has zero size (self-closing), the trigger is used as the reference element.
  • The card is rendered into document.body via a portal and is position: fixed for stable positioning on scroll.
  • Positioning is recomputed on open, scroll, resize, and while the card resizes.

<PopoverTrigger />

Wraps your interactive element that toggles the popover.

PropTypeDefaultDescription
asChildbooleantrueIf true, clones the single child and attaches handlers/refs (prevents nested buttons).
childrenReactElementExactly one element to serve as the trigger.
slStyleLikeOptional style system prop forwarded through the lib’s compileStyle.

When asChild={false}, a semantic wrapper <span role="button" tabIndex={0}> is used to avoid nested <button> elements. Keyboard interaction (Enter/Space) is supported in that case.


<PopoverAnchor />

Optional anchor element. If omitted, the popover uses the trigger as anchor.

PropTypeDefaultDescription
asChildbooleanfalseWhen true, does not render a wrapper; instead it clones the only child to grab its ref.
childrenReactElement | nullnullThe element to act as the anchor. If null, falls back to the trigger.
slStyleLikeOptional style system prop.

<PopoverCard />

The floating card panel. Handles its own positioning and portals to document.body.

PropTypeDefaultDescription
rolestring"dialog"ARIA role for the card.
classNamestringCustom class.
styleReact.CSSPropertiesInline styles (top/left are managed internally).
slStyleLikeStyle system prop.
childrenReactNodeCard content.
...layoutPropsanyAny additional props are forwarded to the styled root.

Implementation details

  • Uses a double requestAnimationFrame on open to ensure layout is available before measuring.
  • Repositions on scroll/resize and when the card or document.body resizes.
  • Applies data-placement attribute for styling hooks.

<PopoverClose />

A convenience button that closes the popover.

PropTypeDefaultDescription
asChildbooleanfalseIf true, clones the single child and wires onClick.
childrenReactNode"✕"Button content.
onClick(e: MouseEvent) => voidOptional; user handler composed with the internal closer.
...restbutton propsForwarded to the <button>.

<PopoverArrow />

Renders the arrow and positions it along the relevant edge of the card. The arrow is drawn with two border triangles (shadow + fill) and is clamped away from rounded corners; on vertical sides it also has a small bottom guard to avoid drifting too close to the bottom edge while scrolling.

PropTypeDefaultDescription
sizenumber8Arrow size (edge to tip).
slStyleLikeStyle system prop for advanced customization.
styleReact.CSSPropertiesInline style (usually you don’t need to set top/left manually).
...restdiv propsForwarded to the styled arrow root.

Styling hooks

  • The arrow root receives data-side="top" \| "bottom" \| "left" \| "right" depending on which edge of the card the arrow attaches to. Use this to provide theme‑specific decorations.

Theming & customization

  • All container components accept the sl style‑engine prop which passes through the design system’s compileStyle helper.
  • The arrow inherits its background from the card and uses a subtle drop shadow; change CSS variables (--ld-surface, --ld-fg, --ld-radius-lg, --ld-shadow-lg) or override the styled parts if needed.
  • Because the card is position: fixed within a portal, it is unaffected by parent transforms and scroll containers—positioning remains consistent.

Caveats

  • Avoid giving the anchor display: none; the popover relies on its DOMRect. If the anchor is zero‑sized, the trigger will be used instead.
  • Don’t nest native interactive elements (e.g., <button> inside another <button>). Use asChild on the trigger to attach events to your own element.