Popover
A small panel anchored to the button that opens it, for content the user acts in without leaving the page: a short form, filters, a share link. It sits on the raised surface with a border and a medium shadow, and closes on Escape, an outside press or its trigger.
Preview
import { Button } from "@/components/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>Usage
When to use
- A few controls next to the button that opens them: a share link, filters, a quick setting.
- Content the user acts in without leaving the page, which closes on Escape or a press outside.
When not to use
- A list of actions. Use DropdownMenu.
- A hint on hover or focus, with no controls. Use Tooltip.
- A long form, or input the user must not lose on a press outside. Use Dialog.
Variants
side
Set on PopoverContent. The side of the trigger it opens on. It flips when there is no room.
import { Button } from "@/components/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent side="top" aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent side="right" aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent side="left" aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>align
Set on PopoverContent. How it lines up with the trigger along that side. Use start or end when the trigger sits near an edge.
import { Button } from "@/components/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent align="start" aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent align="end" aria-label="Share this project" className="flex flex-col gap-md">
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>Examples
Share link
import { Button } from "@/components/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary">Share</Button>
</PopoverTrigger>
<PopoverContent aria-labelledby="share-heading" className="flex flex-col gap-md">
<p id="share-heading" className="text-md font-medium">Share this project</p>
<Field>
<FieldLabel>Link</FieldLabel>
<FieldControl>
<Input readOnly defaultValue="https://example.com/p/42" />
</FieldControl>
</Field>
<PopoverClose asChild>
<Button>Done</Button>
</PopoverClose>
</PopoverContent>
</Popover>Next to an edge
align end keeps the panel on screen when the trigger sits at the right of a toolbar.
import { IconList } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger asChild>
<Button variant="secondary" iconOnly aria-label="Filters">
<IconList size={16} />
</Button>
</PopoverTrigger>
<PopoverContent align="end" aria-label="Filters" className="flex flex-col gap-md">
<Checkbox label="Only my projects" />
<Checkbox label="Show archived" />
</PopoverContent>
</Popover>Do and don't
Do
- Keep it short: a few controls, a link to copy, a set of filters.
- Give the panel a heading, or an aria-label when it has none.
- Apply changes at once, or close it with a PopoverClose button such as Done.
Don't
- Don't put a list of actions in it; use DropdownMenu.
- Don't use it for a long form or anything the user must not lose on an outside press; use Dialog.
- Don't open it on hover; use Tooltip for a hint.
Props
| Name | Type | Values | Default | Description |
|---|---|---|---|---|
| open | boolean | - | - | Set on Popover. Whether it is open, when you control it. Pair it with onOpenChange. |
| defaultOpen | boolean | - | false | Set on Popover. Opens it on first render, when you don't control it. |
| onOpenChange | function | - | - | Set on Popover. Called with true or false when it opens or closes (trigger, Escape, an outside press, PopoverClose). |
| modal | boolean | - | false | Set on Popover. true keeps focus inside and hides the rest of the page from assistive tech until it closes. Leave it false for a panel the user works next to. |
| side | enum | top | right | bottom | left | bottom | Set on PopoverContent. The side of the trigger it opens on. It flips when there is no room. |
| align | enum | start | center | end | center | Set on PopoverContent. How it lines up with the trigger along that side. Use start or end when the trigger sits near an edge. |
| sideOffset | number | - | 4 | Set on PopoverContent. Space between the trigger and the panel, in px. 4 is spacing-xs. |
Slots
| Name | Required | Description |
|---|---|---|
| trigger | yes | PopoverTrigger with asChild around a Button. |
| content | yes | PopoverContent with a heading or an aria-label, and the controls. |
Anatomy
| Part | Description |
|---|---|
| root | Popover. Holds the open state; renders nothing itself. |
| trigger | PopoverTrigger: the button that opens it, usually a Button with asChild. |
| content | PopoverContent: the panel. Raised fill, border, radius-container, shadow-med, px-lg py-md, 288px wide and never wider than the screen minus spacing-sm on each side. |
| anchor | PopoverAnchor (optional): another element to position the panel against. |
| close | PopoverClose (optional): a button inside that closes it, such as Done. |
Accessibility
Role: dialog
| Keys | Action |
|---|---|
| Space, Enter | On the trigger: opens or closes the popover. Focus moves to the first focusable element inside, or to the panel. |
| Tab | Moves through the controls inside. With modal, focus stays inside; otherwise it can leave, and the popover stays open. |
| Escape | Closes the popover and returns focus to the trigger. |
- The trigger gets aria-expanded and aria-controls, and the panel is a dialog. Name it with aria-label or aria-labelledby when it has no heading.
- An outside press closes it. Put anything the user must not lose (unsaved input) in a Dialog instead.
- Not for a list of actions: that is a DropdownMenu, which has menu semantics and arrow keys.
- Not for a hint on hover: that is a Tooltip, which holds no controls.
Install
npx shadcn add https://werewolf-ui.vercel.app/r/popover.jsonThis also adds styles/werewolf.css, the tokens every component uses, and any other component it needs. Import that file once in your root CSS, after @import "tailwindcss", with a path relative to that CSS file (from app/globals.css it is @import "../styles/werewolf.css").