Light

Popover

Displays rich content in a portal, triggered by a button.

Spec · from metadata

When to use

  • Rich interactive content anchored to a trigger (mini forms, color pickers, date pickers)
  • Filter controls or settings that don't warrant a full dialog
  • Any floating content that requires user interaction (clicks, inputs)
  • Content that needs a title/description structure in a floating panel

When not to use

  • Simple text labels on hover -- use Tooltip instead
  • Preview cards on hover -- use HoverCard instead
  • Action menus -- use DropdownMenu instead
  • Full blocking modals -- use Dialog or AlertDialog
  • Content that should persist inline -- use collapsible or card

Anti-patterns

Avoid// Plain text tooltip using Popover
<Popover>
  <PopoverTrigger>
    <IconButton />
  </PopoverTrigger>
  <PopoverContent>Save document</PopoverContent>
</Popover>
Prefer<Tooltip>
  <TooltipTrigger asChild>
    <IconButton />
  </TooltipTrigger>
  <TooltipContent>Save document</TooltipContent>
</Tooltip>

Tooltip is the correct component for simple text labels. It appears on hover, is lighter weight, and has proper tooltip ARIA semantics.

Avoid// Action menu using Popover
<Popover>
  <PopoverContent>
    <button>Edit</button>
    <button>Delete</button>
  </PopoverContent>
</Popover>
Prefer<DropdownMenu>
  <DropdownMenuContent>
    <DropdownMenuItem>Edit</DropdownMenuItem>
    <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>

DropdownMenu provides proper menu ARIA role, keyboard navigation (arrow keys), and item semantics. Popover lacks menu semantics.

Accessibility

  • Screen readerContent is portaled but associated with the trigger via Radix. Use PopoverTitle and PopoverDescription for labeling.
  • ContrastUses bg-popover/text-popover-foreground token pair for proper contrast.

Token bindings

TokenCategoryUsage
bg-popovercolorPopover background
text-popover-foregroundcolorPopover text
text-muted-foregroundcolorDescription text
shadow-mdshadowPopover elevation
rounded-mdradiusPopover border radius
p-4spacingContent padding

Import

import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@timelycare/helix-ui"

Props

interface PopoverProps {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  modal?: boolean
}

interface PopoverContentProps {
  align?: "start" | "center" | "end"
  side?: "top" | "right" | "bottom" | "left"
  sideOffset?: number
  className?: string
  children: React.ReactNode
}

Structure

PartTailwind
PopoverContentbg-popover text-popover-foreground border rounded-md shadow-md p-4 w-80
Header titlefont-semibold text-base leading-none text-popover-foreground
Header descriptiontext-sm text-muted-foreground
Header spacingspace-y-2 (gap between title and description)
Content spacinggap-4 (between header and content sections)

Alignment

PropValuesDefault
alignstart, center, endcenter
sidetop, right, bottom, leftbottom
sideOffsetnumber (px)4

Common Patterns

Basic Popover

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">Open</Button>
  </PopoverTrigger>
  <PopoverContent>
    <p className="text-sm">Content goes here.</p>
  </PopoverContent>
</Popover>

With Header

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">Settings</Button>
  </PopoverTrigger>
  <PopoverContent className="w-80">
    <div className="space-y-2">
      <h4 className="font-semibold leading-none">Dimensions</h4>
      <p className="text-sm text-muted-foreground">
        Set the dimensions for the layer.
      </p>
    </div>
  </PopoverContent>
</Popover>

With Form Grid

<Popover>
  <PopoverTrigger asChild>
    <Button variant="outline">Dimensions</Button>
  </PopoverTrigger>
  <PopoverContent className="w-80">
    <div className="flex flex-col gap-4">
      <div className="space-y-2">
        <h4 className="font-semibold leading-none">Dimensions</h4>
        <p className="text-sm text-muted-foreground">
          Set the dimensions for the layer.
        </p>
      </div>
      <div className="grid gap-2">
        <div className="grid grid-cols-3 items-center gap-4">
          <Label htmlFor="width" className="text-right">Width</Label>
          <Input id="width" className="col-span-2 h-8" />
        </div>
        <div className="grid grid-cols-3 items-center gap-4">
          <Label htmlFor="height" className="text-right">Height</Label>
          <Input id="height" className="col-span-2 h-8" />
        </div>
      </div>
    </div>
  </PopoverContent>
</Popover>

Controlled

const [open, setOpen] = useState(false)

<Popover open={open} onOpenChange={setOpen}>
  <PopoverTrigger asChild>
    <Button>Toggle</Button>
  </PopoverTrigger>
  <PopoverContent>
    <Button size="sm" onClick={() => setOpen(false)}>Close</Button>
  </PopoverContent>
</Popover>

Accessibility

  • Built on Radix Popover — handles focus management automatically
  • Keyboard: Enter/Space to toggle, Escape to close
  • Focus moves into popover content when opened
  • Screen reader: content accessible when popover is open
  • Returns focus to trigger on close

Gotchas

ProblemSolution
Trigger not clickableAdd asChild prop to PopoverTrigger
Content width variesSet explicit className="w-80"
Form labels misalignedUse grid grid-cols-3 with text-right on labels
Z-index conflictsPopoverContent uses z-50 by default

See Also


Last updated: February 9, 2026