Popover

A non-modal floating panel built on Radix UI Popover that appears relative to a trigger element. Supports controlled and uncontrolled open state, configurable alignment, side placement, and offset — suitable for tooltips, inline forms, pickers, and settings panels.

Installation

npm install @designforge/ui

Usage

import {
  Popover,
  PopoverTrigger,
  PopoverAnchor,
  PopoverContent,
} from "@/components/ui/popover"

Examples

Basic Popover

A minimal popover that opens when the trigger button is clicked.

import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
export function BasicPopover() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Open Popover</Button>
      </PopoverTrigger>
      <PopoverContent>
        <p className="text-sm text-muted-foreground">
          This is a simple popover with some descriptive text.
        </p>
      </PopoverContent>
    </Popover>
  )
}

Form Inside Popover

Embed a form directly inside the popover content for compact inline editing flows.

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
export function PopoverForm() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Edit Profile</Button>
      </PopoverTrigger>
      <PopoverContent className="w-72">
        <div className="grid gap-4">
          <div className="space-y-1">
            <h4 className="font-medium leading-none">Profile</h4>
            <p className="text-sm text-muted-foreground">
              Update your display name and username.
            </p>
          </div>
          <div className="grid gap-2">
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="display-name">Name</Label>
              <Input
                id="display-name"
                defaultValue="Mayank"
                className="col-span-2 h-8"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="username">Username</Label>
              <Input
                id="username"
                defaultValue="@mayank_dev"
                className="col-span-2 h-8"
              />
            </div>
          </div>
          <Button size="sm">Save changes</Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Date Range Picker Style

Use align="start" to anchor the popover to the leading edge of the trigger — the natural position for a date-range picker.

import { CalendarIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
export function DateRangePopover() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          className="w-[260px] justify-start text-left font-normal"
        >
          <CalendarIcon className="mr-2 h-4 w-4" />
          <span className="text-muted-foreground">Pick a date range</span>
        </Button>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-auto p-0">
        <div className="p-4 text-sm text-muted-foreground">
          Calendar placeholder — wire up your preferred date picker here.
        </div>
      </PopoverContent>
    </Popover>
  )
}

Color Picker

Place a color swatch grid inside the popover for quick palette selection.

import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
const COLORS = [
  "#ef4444", "#f97316", "#eab308", "#22c55e",
  "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
  "#6b7280", "#1f2937",
]
 
export function ColorPickerPopover() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Pick a color</Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto">
        <div className="grid grid-cols-5 gap-2 p-1">
          {COLORS.map((color) => (
            <button
              key={color}
              className="h-7 w-7 rounded-md border border-border focus:outline-none focus:ring-2 focus:ring-ring"
              style={{ backgroundColor: color }}
              aria-label={color}
            />
          ))}
        </div>
      </PopoverContent>
    </Popover>
  )
}

Settings Panel

Use side="bottom" with align="end" to anchor a settings popover to the trailing edge of a toolbar button.

import { Settings2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
export function SettingsPopover() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button size="icon" variant="ghost" aria-label="Open settings">
          <Settings2 className="h-4 w-4" />
        </Button>
      </PopoverTrigger>
      <PopoverContent side="bottom" align="end" className="w-72">
        <div className="space-y-3">
          <h4 className="font-medium text-sm">Display Settings</h4>
          <div className="flex items-center justify-between">
            <Label htmlFor="compact-mode" className="text-sm">
              Compact mode
            </Label>
            <Switch id="compact-mode" />
          </div>
          <div className="flex items-center justify-between">
            <Label htmlFor="show-hints" className="text-sm">
              Show hints
            </Label>
            <Switch id="show-hints" defaultChecked />
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Controlled Open State

Manage open state externally to programmatically show or hide the popover.

import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
 
export function ControlledPopover() {
  const [open, setOpen] = useState(false)
 
  return (
    <div className="flex items-center gap-4">
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button variant="outline">
            {open ? "Close" : "Open"} Popover
          </Button>
        </PopoverTrigger>
        <PopoverContent>
          <p className="text-sm">
            This popover is controlled by external state.
          </p>
          <Button
            size="sm"
            variant="ghost"
            className="mt-2"
            onClick={() => setOpen(false)}
          >
            Dismiss
          </Button>
        </PopoverContent>
      </Popover>
      <Button variant="secondary" onClick={() => setOpen(true)}>
        Open via external button
      </Button>
    </div>
  )
}

API Reference

Popover

The root component that manages open/close state.

PropTypeDefaultDescription
openbooleanControlled open state.
onOpenChange(open: boolean) => voidCallback fired when the open state changes.
defaultOpenbooleanfalseInitial open state when uncontrolled.

PopoverTrigger

Wraps the element that toggles the popover. Use asChild to forward rendering to a custom element.

PropTypeDefaultDescription
asChildbooleanfalseMerges props onto the immediate child element instead of a <button>.

PopoverAnchor

An optional positioning anchor separate from the trigger. Useful when the trigger and the visual anchor point differ.

PopoverContent

The floating panel rendered inside a portal.

PropTypeDefaultDescription
align"start" | "center" | "end""center"Alignment of the content relative to the trigger.
side"top" | "right" | "bottom" | "left""bottom"Preferred side to render the content on.
sideOffsetnumber4Pixel distance between the trigger and the content.
classNamestringAdditional CSS classes. Default width is w-72 (288px).

Accessibility

  • Follows the WAI-ARIA Dialog (Non-Modal) pattern via Radix UI.
  • The trigger element receives aria-expanded and aria-controls automatically.
  • Focus is moved into the popover content on open and returned to the trigger on close.
  • Pressing Escape closes the popover and returns focus to the trigger.
  • Clicking outside the popover dismisses it.
  • Use asChild on PopoverTrigger to ensure the accessible role and keyboard behaviour of the trigger element is preserved.

Live View

Open in Storybook ↗