agentic-ui logoagentic-ui

Popover

Floating content panel anchored to a trigger. Scales from the trigger's edge, repositions automatically to avoid viewport overflow.

High-level wrapper

import { Button } from "@brijbyte/agentic-ui/button";
import { Popover } from "@brijbyte/agentic-ui/popover";

import "@brijbyte/agentic-ui/button.css";
import "@brijbyte/agentic-ui/popover.css";

export default function HighLevelDemo() {
  return (
    <>
      <Popover
        trigger={
          <Button variant="outline" size="sm">
            Commit info
          </Button>
        }
        title="abc12ef"
        description="Merged 3 minutes ago by @brijesh into main. Deployment triggered automatically."
        side="bottom"
        align="start"
      />

      <Popover trigger={<Button size="sm">Quick settings</Button>} title="Display settings" dismissible side="bottom">
        <div style={{ marginTop: "var(--space-3)", display: "flex", flexDirection: "column", gap: "var(--space-2)" }}>
          <label
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
              gap: "var(--space-4)",
              fontFamily: "var(--font-mono)",
              fontSize: "var(--font-size-sm)",
              color: "var(--color-secondary)",
            }}
          >
            Compact mode
            <input type="checkbox" />
          </label>
          <label
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
              gap: "var(--space-4)",
              fontFamily: "var(--font-mono)",
              fontSize: "var(--font-size-sm)",
              color: "var(--color-secondary)",
            }}
          >
            Show timestamps
            <input type="checkbox" defaultChecked />
          </label>
        </div>
      </Popover>
    </>
  );
}

Sides

import { Button } from "@brijbyte/agentic-ui/button";
import { Popover } from "@brijbyte/agentic-ui/popover";

import "@brijbyte/agentic-ui/button.css";
import "@brijbyte/agentic-ui/popover.css";

export default function SidesDemo() {
  return (
    <>
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <Popover
          key={side}
          trigger={
            <Button variant="outline" size="sm">
              {side.charAt(0).toUpperCase() + side.slice(1)}
            </Button>
          }
          title={`Opens on ${side}`}
          description="The popup scales from the trigger's edge, not from center."
          side={side}
        />
      ))}
    </>
  );
}

With arrow

import { Button } from "@brijbyte/agentic-ui/button";
import { Popover } from "@brijbyte/agentic-ui/popover";

import "@brijbyte/agentic-ui/button.css";
import "@brijbyte/agentic-ui/popover.css";

export default function WithArrowDemo() {
  return (
    <>
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <Popover
          key={side}
          trigger={
            <Button variant="outline" size="sm">
              {side.charAt(0).toUpperCase() + side.slice(1)}
            </Button>
          }
          title="Arrow popover"
          description="The arrow always points back to the trigger, regardless of side or alignment."
          side={side}
        />
      ))}
    </>
  );
}

Shared trigger

Alice Chen
Marcus Webb
Priya Nair
import { Popover as BasePopover } from "@base-ui/react/popover";
import { Button } from "@brijbyte/agentic-ui/button";
import { PopoverPopup, PopoverTitle, PopoverDescription, PopoverArrow, PopoverViewport } from "@brijbyte/agentic-ui/popover";

import "@brijbyte/agentic-ui/button.css";
import "@brijbyte/agentic-ui/popover.css";

interface Member {
  id: string;
  name: string;
  role: string;
  joined: string;
}

const MEMBERS: Member[] = [
  { id: "1", name: "Alice Chen", role: "Staff Engineer", joined: "Jan 2021" },
  { id: "2", name: "Marcus Webb", role: "Product Designer", joined: "Mar 2022" },
  { id: "3", name: "Priya Nair", role: "Engineering Manager", joined: "Aug 2020" },
];

// One handle shared across all triggers — clicking a new trigger
// transitions the content without closing and reopening the popup.
const memberPopover = BasePopover.createHandle<Member>();

export default function SharedTriggerDemo() {
  return (
    <>
      {/* Single Root wired to the handle. Children is a render function
          that receives the active trigger's payload. */}
      <BasePopover.Root handle={memberPopover}>
        {({ payload }) =>
          payload ? (
            <BasePopover.Portal>
              <BasePopover.Positioner side="right" sideOffset={8} arrowPadding={8}>
                <PopoverPopup>
                  <PopoverArrow />
                  {/* Viewport handles the directional slide when switching triggers */}
                  <PopoverViewport>
                    <PopoverTitle>{payload.name}</PopoverTitle>
                    <PopoverDescription>
                      {payload.role} · Joined {payload.joined}
                    </PopoverDescription>
                  </PopoverViewport>
                </PopoverPopup>
              </BasePopover.Positioner>
            </BasePopover.Portal>
          ) : null
        }
      </BasePopover.Root>

      {/* Each row passes its own member as the payload */}
      <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-1)", width: 260 }}>
        {MEMBERS.map((member) => (
          <div
            key={member.id}
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
              padding: "var(--space-2) var(--space-3)",
              borderRadius: "var(--radius-md)",
              border: "1px solid var(--color-line)",
              background: "var(--color-elevated)",
            }}
          >
            <span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--font-size-sm)", color: "var(--color-primary)" }}>
              {member.name}
            </span>
            <BasePopover.Trigger
              handle={memberPopover}
              payload={member}
              render={
                <Button variant="ghost" size="xs">
                  Info
                </Button>
              }
            />
          </div>
        ))}
      </div>
    </>
  );
}

Composed: base-ui Root + styled parts

import { Popover as BasePopover } from "@base-ui/react/popover";
import { Button } from "@brijbyte/agentic-ui/button";
import { PopoverPopup, PopoverTitle, PopoverDescription, PopoverClose, PopoverArrow } from "@brijbyte/agentic-ui/popover";

import "@brijbyte/agentic-ui/button.css";
import "@brijbyte/agentic-ui/popover.css";

export default function ComposedDemo() {
  return (
    <BasePopover.Root>
      <BasePopover.Trigger
        render={
          <Button variant="outline" size="sm">
            User info
          </Button>
        }
      />
      <BasePopover.Portal>
        <BasePopover.Positioner side="bottom" align="start" sideOffset={8} arrowPadding={8}>
          <PopoverPopup>
            <PopoverArrow />
            <PopoverClose aria-label="Close" />
            <PopoverTitle>Jane Smith</PopoverTitle>
            <PopoverDescription>Staff Engineer · Platform · she/her</PopoverDescription>
          </PopoverPopup>
        </BasePopover.Positioner>
      </BasePopover.Portal>
    </BasePopover.Root>
  );
}

API Reference

Floating content panel anchored to a trigger. Scales from the trigger's
edge and repositions automatically to avoid viewport overflow. Supports
title, description, close button, and arbitrary body content.

PropTypeDefault
trigger*ReactElement<unknown, string | JSXElementConstructor<any>>

The trigger element — rendered as-is, receives the popover open/close handler.

titleReactNode

Optional heading rendered at the top of the popup.

descriptionReactNode

Optional supporting text rendered below the title.

childrenReactNode

Body content of the popover.

side"top" | "bottom" | "left" | "right"bottom

Which side of the trigger to open on.

align"start" | "center" | "end"center

Alignment relative to the trigger.

sideOffsetnumber8

Gap between trigger and popup in px.

dismissiblebooleanfalse

Show a close (×) button in the top-right corner.

openboolean

Controlled open state.

defaultOpenboolean

Uncontrolled default open state.

onOpenChange((open: boolean, eventDetails: unknown) => void)

Called when the popover opens or closes.

Styled Parts

base-ui docs ↗

Pre-styled wrappers around the corresponding base-ui parts. All base-ui props are forwarded.

CSS Class Names

Available as keys on the PopoverStyles object. Each key maps to a hashed CSS module class name at runtime.

arrowarrow-fillarrow-seamarrow-strokeclosedescriptionpopuppositionertitleviewport