1
ComponentsOverlays

Popover

Contextual information, compact actions and lightweight forms anchored to a trigger.

Purpose

Use Popover for short contextual interactions, supporting information or compact forms anchored to a trigger without leaving the current workflow.

Form popover

import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverClose,
  PopoverDescription,
  PopoverPopup,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";

export function Example() {
  const [open, setOpen] = useState(false);

  function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setOpen(false);
  }

  return (
    <Popover onOpenChange={setOpen} open={open}>
      <PopoverTrigger render={<Button variant="outline" />}>
        Invite member
      </PopoverTrigger>
      <PopoverPopup className="w-72">
        <Form className="grid gap-3" onSubmit={onSubmit}>
          <div className="grid gap-1">
            <PopoverTitle>Invite member</PopoverTitle>
            <PopoverDescription>Send a role-limited invitation.</PopoverDescription>
          </div>
          <Field name="email">
            <FieldLabel>Email</FieldLabel>
            <Input placeholder="maya@company.com" required type="email" />
          </Field>
          <div className="flex justify-end gap-2">
            <PopoverClose render={<Button type="button" variant="ghost" />}>
              Cancel
            </PopoverClose>
            <Button type="submit">Send</Button>
          </div>
        </Form>
      </PopoverPopup>
    </Popover>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/popover

Import

import { Popover } from "@/components/ui/popover";
import { PopoverClose, PopoverContent, PopoverCreateHandle, PopoverDescription, PopoverPopup, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
import { Field } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";

Need the machine-readable source metadata? View registry JSON.

Usage

Use Popover for small contextual forms and helpers anchored to a trigger. Use Dialog for larger modal tasks.

import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverClose,
  PopoverDescription,
  PopoverPopup,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";

export function Example() {
  const [open, setOpen] = useState(false);

  function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setOpen(false);
  }

  return (
    <Popover onOpenChange={setOpen} open={open}>
      <PopoverTrigger render={<Button variant="outline" />}>
        Invite member
      </PopoverTrigger>
      <PopoverPopup className="w-72">
        <Form className="grid gap-3" onSubmit={onSubmit}>
          <div className="grid gap-1">
            <PopoverTitle>Invite member</PopoverTitle>
            <PopoverDescription>Send a role-limited invitation.</PopoverDescription>
          </div>
          <Field name="email">
            <FieldLabel>Email</FieldLabel>
            <Input placeholder="maya@company.com" required type="email" />
          </Field>
          <div className="flex justify-end gap-2">
            <PopoverClose render={<Button type="button" variant="ghost" />}>
              Cancel
            </PopoverClose>
            <Button type="submit">Send</Button>
          </div>
        </Form>
      </PopoverPopup>
    </Popover>
  );
}

Examples

Placement, close buttons, the tooltip style, detached triggers and practical compositions are props and compositions of the same primitive.

Basic

<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
  <PopoverPopup className="w-72">
    <div className="grid gap-2">
      <PopoverTitle className="text-base">Workspace details</PopoverTitle>
      <PopoverDescription>
        Quick context without leaving the current workflow.
      </PopoverDescription>
    </div>
  </PopoverPopup>
</Popover>

Controlled

const [open, setOpen] = useState(false);

<Popover onOpenChange={setOpen} open={open}>
  <PopoverTrigger render={<Button variant="outline" />}>
    {open ? "Close info" : "Open info"}
  </PopoverTrigger>
  <PopoverPopup className="w-72">
    <PopoverTitle className="text-base">Controlled popover</PopoverTitle>
    <PopoverDescription>
      Local state controls the open and close flow.
    </PopoverDescription>
    <Button onClick={() => setOpen(false)}>Done</Button>
  </PopoverPopup>
</Popover>

Placement

side and align position the popup around the trigger; sideOffset and alignOffset fine-tune the distance.

{/* side: "top" | "bottom" | "left" | "right", default "bottom" */}
{/* align: "start" | "center" | "end", default "start" */}
<PopoverPopup align="center" side="top" sideOffset={4} alignOffset={0}>
  <PopoverDescription>Opens on the top side of the trigger.</PopoverDescription>
</PopoverPopup>

With close button

<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
  <PopoverPopup className="w-80">
    <PopoverClose
      aria-label="Close"
      className="absolute end-2 top-2"
      render={<Button size="icon-xs" variant="ghost" />}
    >
      <XIcon />
    </PopoverClose>
    <div className="mb-3 grid gap-1 pr-7">
      <PopoverTitle className="text-base">Notifications</PopoverTitle>
      <PopoverDescription>You are all caught up. Good job!</PopoverDescription>
    </div>
    <PopoverClose render={<Button variant="outline" />}>Close</PopoverClose>
  </PopoverPopup>
</Popover>

Tooltip style

tooltipStyle switches the popup to compact tooltip sizing. Combined with openOnHover it works as a rich hover hint.

<InputGroup className="max-w-72">
  <InputGroupInput aria-label="Password" placeholder="Password" type="password" />
  <InputGroupAddon align="inline-end">
    <Popover>
      <PopoverTrigger
        openOnHover
        render={
          <Button aria-label="Password requirements" size="icon-xs" variant="ghost" />
        }
      >
        <InfoIcon />
      </PopoverTrigger>
      <PopoverPopup side="top" tooltipStyle>
        <p>Min. 8 characters</p>
      </PopoverPopup>
    </Popover>
  </InputGroupAddon>
</InputGroup>

Detached triggers

PopoverCreateHandle connects detached triggers to one shared popup that morphs between payloads.

const detachedPopoverHandle = PopoverCreateHandle<ComponentType>();

<PopoverTrigger
  handle={detachedPopoverHandle}
  payload={NotificationsContent}
  render={<Button aria-label="Notifications" size="icon" variant="outline" />}
>
  <BellIcon aria-hidden="true" />
</PopoverTrigger>
<PopoverTrigger
  handle={detachedPopoverHandle}
  payload={ProfileContent}
  render={<Button aria-label="Profile" size="icon" variant="outline" />}
>
  <UserIcon aria-hidden="true" />
</PopoverTrigger>
<Popover handle={detachedPopoverHandle}>
  {({ payload: Payload }) => (
    <PopoverPopup>{Payload !== undefined && <Payload />}</PopoverPopup>
  )}
</Popover>

Quick actions

<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
  <PopoverPopup className="w-64">
    <div className="grid gap-3">
      <PopoverTitle className="text-base">Quick actions</PopoverTitle>
      <div className="grid grid-cols-3 gap-2">
        <QuickActionButton icon={<PencilIcon />} label="Edit" />
        <QuickActionButton icon={<CopyIcon />} label="Copy" />
        <QuickActionButton icon={<Share2Icon />} label="Share" />
      </div>
    </div>
  </PopoverPopup>
</Popover>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • Compose Popover, PopoverTrigger and PopoverPopup; the popup already owns its portal and positioner.
  • PopoverPopup accepts side, align, sideOffset, alignOffset, anchor, portalProps and the popup content props.
  • Use PopoverTitle, PopoverDescription and PopoverClose for structured, dismissible content.
  • PopoverCreateHandle supports detached triggers or externally controlled popup access when the normal trigger relationship is not sufficient.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • PopoverPopup uses the shared floating-panel fade, scale and directional offset tokens.
  • Collision handling, transform origin and reduced-motion behavior remain part of the native Siteplane floating-panel contract.

Accessibility

  • Escape, outside click and focus management are provided by Base UI and must remain intact.
  • Give icon-only triggers an accessible name and use PopoverTitle and PopoverDescription for structured content.
  • Use Tooltip for a non-interactive hint and Dialog or Alert Dialog for long or critical tasks.

Implementation Guidance

  • Keep portal, positioning, focus and dismissal logic inside the primitive.
  • Prefer the regular trigger relationship; use anchor or PopoverCreateHandle only for a genuine detached-anchor requirement.
  • Do not use Popover for irreversible decisions or long forms.

On This Page