Popover

Free · MIT

Anchored popover with an arrow in default, soft, glass, neo and inverted surfaces, tones, sizes, radii and shadows; side and align that flip and shift to stay on screen, match-trigger-width, scale, fade, slide or spring entrances, header, close button and footer slots, and modal (focus trapped) or non-modal (Tab flows back to the page) behavior, shown as a date filter, share menu, switcher and inline note form.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/popover.json

The file lands in components/goodui/popover.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/popover works too.

Usage

import * as React from "react"
import { Calendar, ChevronDown } from "lucide-react"
import { Button } from "@/components/goodui/button"
import { Popover } from "@/components/goodui/popover"

export function DateFilter({ value, onChange }: { value: string; onChange: (v: string) => void }) {
  const [draft, setDraft] = React.useState(value)
  return (
    <Popover
      align="start"
      title="Date range"
      onOpenChange={(open) => open && setDraft(value)}
      trigger={
        <Button variant="outline" iconLeft={<Calendar />} iconRight={<ChevronDown />}>
          {value}
        </Button>
      }
      footer={({ close }) => (
        <Button size="sm" onClick={() => { onChange(draft); close() }}>
          Apply
        </Button>
      )}
    >
      <Presets value={draft} onChange={setDraft} />
    </Popover>
  )
}

When to use

Use it for
  • Filters next to the control that opens them: a date range, a status filter, a location switcher.
  • Small actions on one item: share a link, add a note to a booking, set a reminder.
  • Extra information or settings that need buttons or fields, which a tooltip can't hold.
  • Account or workspace switchers that should be exactly as wide as their trigger (matchTriggerWidth).
Reach for something else
  • Picking one value from a list: use select; it has listbox keyboard behavior and form support.
  • A task that must be finished or cancelled before anything else: use dialog.
  • A hint on hover: use tooltip.
  • A menu of commands: use dropdown-stagger; menus use arrow keys, popovers use Tab.
If you needUse
Filters and switchersvariant="default", align="start"
Inline forms that shouldn't lose input to a stray clickmodal (focus trapped, page blocked until saved or dismissed)
Switchers and comboboxesmatchTriggerWidth with arrow={false}
Highlighted tipsvariant="soft" with a tone
Dark calloutsvariant="inverted"
Over photos or gradientsvariant="glass"
Playful brandsvariant="neo"

Examples

Share menu that closes after an action

<Popover title="Share booking page" size="sm" align="end" trigger={<Button>Share</Button>}>
  {({ close }) => <ShareTargets onShared={close} />}
</Popover>

Modal mini form

<Popover
  modal
  side="start"
  variant="soft"
  tone="warning"
  title="Note for Maya"
  trigger={<Button size="sm" variant="ghost">Note</Button>}
  footer={({ close }) => <Button size="sm" onClick={() => { save(); close() }}>Save note</Button>}
>
  <textarea aria-label="Note" />
</Popover>

Location switcher as wide as its trigger

<Popover matchTriggerWidth arrow={false} align="start" aria-label="Switch location" trigger={<Button className="w-56">Downtown</Button>}>
  {({ close }) => <LocationList onPick={close} />}
</Popover>

API reference

PropTypeDefault
trigger*
The element that toggles the popover (a button). It gets aria-expanded, aria-controls and aria-haspopup.
React.ReactElement<TriggerProps>—
children
Body. A function receives { close } for buttons that should close the popover.
React.ReactNode | ((api: PopoverApi) => React.ReactNode)—
title
Header title; also names the popover for screen readers. Without it, pass aria-label.
React.ReactNode—
description
Muted line under the title.
React.ReactNode—
footer
Footer row (actions). A function receives { close }.
React.ReactNode | ((api: PopoverApi) => React.ReactNode)—
showClose
× button in the header. Defaults to true when there is a title.
boolean—
variant
Surface: default (your popover color), soft (tone tint), glass, neo or inverted (dark on light).
"default" | "soft" | "glass" | "neo" | "inverted""default"
tone"primary" | "neutral" | "success" | "warning" | "danger" | "info""primary"
size
Width and padding: sm 16rem, md 20rem, lg 26rem, auto fits the content.
"sm" | "md" | "lg" | "auto""md"
radius"none" | "sm" | "md" | "lg" | "xl""lg"
shadow
Drop shadow. Ignored by neo, which has its own offset shadow.
"none" | "sm" | "md" | "lg" | "xl""lg"
animation
How it opens: scale (default), fade, slide, spring or none. Reduced motion uses a short fade.
"none" | "fade" | "scale" | "slide" | "spring""scale"
side
Preferred side; flips to the opposite side when there is no room.
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
Alignment along the side. start/end follow the reading direction.
"start" | "center" | "end""center"
offset
Gap between trigger and popover, in px.
number8
arrowbooleantrue
avoidCollisions
false keeps the requested side and alignment even when the popover leaves the screen.
booleantrue
matchTriggerWidth
Same width as the trigger (account switchers, comboboxes). Overrides size width.
booleanfalse
modal
true traps focus inside and blocks the page behind with a transparent layer.
booleanfalse
closeOnOutsideClickbooleantrue
closeOnEscbooleantrue
open
Open state (controlled).
boolean—
defaultOpen
Initial open state when uncontrolled.
booleanfalse
onOpenChange(open: boolean) => void—
initialFocus
Element to focus on open. Default: the first focusable element in the body, else the panel.
React.RefObject<HTMLElement | null>—
returnFocus
Return focus to the trigger after closing with Esc, the close button or close().
booleantrue
portal
Render into document.body so overflow and z-index of parents can't clip it.
booleantrue
labels
Text for built-in controls; pass translations here.
Partial<PopoverLabels>—

Also accepts every <div> attribute.

Accessibility

Enter / SpaceOn the trigger: opens or closes the popover (native button). Focus moves to the first control in the body.
Tab / Shift+TabNon-modal: moves through the popover as if it sat right after the trigger; Tab past the last control closes it and focuses the next control on the page, Shift+Tab before the first closes it and returns to the trigger. Modal: wraps around inside.
EscCloses the popover (not a dialog behind it) and returns focus to the trigger.
  • The panel has role="dialog", is named by the title (aria-labelledby) and described by the description; without a title, pass aria-label. modal adds aria-modal="true".
  • The trigger gets aria-haspopup="dialog", aria-expanded and aria-controls.
  • Focus moves in on open (initialFocus, else the first control in the body, else the panel) and returns to the trigger after Esc, the close button or close(). An outside click doesn't pull focus back, so the clicked control keeps it.
  • Outside-click detection follows the React tree, so popovers, selects and menus rendered in portals inside the popover don't close it.
  • The × button is named by labels.close for translation. The arrow is decorative.

Motion

  • scale: from 0.94 in 180 ms. slide: 6px from the trigger in 220 ms. spring: from 0.8 on a spring (stiffness 520, damping 26). fade: opacity only, 150 ms. Every variant leaves in 120 ms.
  • Scale and spring grow from the arrow tip.
  • Reduced motion: a 100 ms fade, no movement. animation="none" opens and closes instantly.
  • Nothing renders until it opens. Position is recomputed on open, scroll, resize and when the panel or trigger changes size, not on every frame.
  • The body scrolls when the space on the chosen side is smaller than the content, instead of the panel leaving the screen.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • Pass the opener as trigger={<Button>…</Button>} and the body as children. Use footer for actions.
  • children and footer can be functions receiving { close }; call close() after applying or saving.
  • Prefer side="start" / "end" over left/right so RTL works. The popover flips and shifts by itself; pass avoidCollisions={false} only when you really need a fixed side.
  • Don't put a Popover trigger inside another button. Don't use it for menus or selects.
  • Give it a title or an aria-label.

FAQ

Can it be controlled?

Yes: open, defaultOpen and onOpenChange. onOpenChange(true) is a good place to reset a draft to the saved value.

Does it work inside a dialog?

Yes. It renders into document.body above the dialog, Esc closes only the popover, and clicks inside it don't count as clicks outside the dialog.

Why does the body scroll?

When the space on the chosen side is smaller than the popover, it gets a max height and the body scrolls.

Related

Dialog & Sheet

Modal dialog, alert dialog and sheet in one: default, soft, glass and neo surfaces in six tones; center, top, start/end/left/right and draggable bottom sheets, fullscreen, and a phone override to fullscreen or sheet; scale, slide, blur, fade or morph-from-trigger entrances; async confirm with spinner and inline error, type-to-confirm, nested dialogs that step back, focus trap and restore, and scroll lock without layout shift.

Tooltip

Dark, light, tone, glass and neo tooltips with sizes, shapes, shadows and an optional arrow; twelve placements that flip and shift at the screen edge, scale, fade, slide or spring entrances, rich title + keyboard-shortcut content, delay groups that open instantly after the first, follow-cursor for charts and touch long-press.

Select

Fully custom single or multi select: outline, filled, ghost, underline, pill and neo triggers; icons, avatars, descriptions and groups; search with match highlight, chips, clear, type-ahead and full keyboard; animated menu that flips up when space runs out.

Button

One button with ten variants, six tones, seven shadows, ten animations, three shapes and five sizes that all combine, plus loading and icon slots.

Popover morph

A small button such as "Add note" morphs into a floating form panel, its label sliding into the header, and shrinks back into the button on save, Esc or an outside click.

Stagger dropdown

A dropdown menu springs open while its items stagger in, with a highlight that glides between items, submenus, checkbox and radio items, and full keyboard navigation.