PopoverDrawer
An anchored Popover at desktop that becomes a swipeable Drawer below 768px.
A drop-in replacement for the shadcn Popover: at 768px and up it renders your Popover unchanged; below, the same children render inside your Drawer. The breakpoint and the override prop work the same for every pair, so this page covers only what is specific to this one.
At 768px and up: a non-modal Popover anchored to the button. Resize the window across 768px while it is open: it stays open on the other side.
import {
PopoverDrawer,
PopoverDrawerContent,
PopoverDrawerDescription,
PopoverDrawerHeader,
PopoverDrawerTitle,
PopoverDrawerTrigger,
} from "@/components/ui/popover-drawer"
export function Dimensions() {
return (
<PopoverDrawer>
<PopoverDrawerTrigger>Open popover</PopoverDrawerTrigger>
<PopoverDrawerContent className="w-80">
<PopoverDrawerHeader>
<PopoverDrawerTitle>Dimensions</PopoverDrawerTitle>
<PopoverDrawerDescription>
Set the dimensions for the layer.
</PopoverDrawerDescription>
</PopoverDrawerHeader>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<label htmlFor="width">Width</label>
<input id="width" defaultValue="100%" className="col-span-2" />
</div>
<div className="grid grid-cols-3 items-center gap-4">
<label htmlFor="height">Height</label>
<input id="height" defaultValue="25px" className="col-span-2" />
</div>
</div>
</PopoverDrawerContent>
</PopoverDrawer>
)
}Installation
npx shadcn@latest add @alexandrejoly/popover-drawerUpdate the import paths to match your project setup.
Migrating
Change the imports and insert Drawer after Popover in each part name:
PopoverContent becomes PopoverDrawerContent, and so on, with the same
props and children (see Migrating). Then
check two things below 768px: the
desktop-only props, and a title on every content (see
Accessibility).
Usage
import {
PopoverDrawer,
PopoverDrawerContent,
PopoverDrawerDescription,
PopoverDrawerHeader,
PopoverDrawerTitle,
PopoverDrawerTrigger,
} from "@/components/ui/popover-drawer"<PopoverDrawer>
<PopoverDrawerTrigger>Open popover</PopoverDrawerTrigger>
<PopoverDrawerContent>
<PopoverDrawerHeader>
<PopoverDrawerTitle>Title</PopoverDrawerTitle>
<PopoverDrawerDescription>Description</PopoverDrawerDescription>
</PopoverDrawerHeader>
</PopoverDrawerContent>
</PopoverDrawer>Composition
PopoverDrawer
├── PopoverDrawerAnchor (Radix, optional)
├── PopoverDrawerTrigger
└── PopoverDrawerContent
├── PopoverDrawerHeader
│ ├── PopoverDrawerTitle
│ └── PopoverDrawerDescription
└── PopoverDrawerFooter (optional)The content box
Below 768px, PopoverDrawerContent wraps its children in a padded box with
the Popover's column layout, so children written for the Popover render the
same way in the Drawer. The className you pass to PopoverDrawerContent
styles that box; drawer={{ className }} styles the drawer itself:
At 768px and up: a 320px Popover.
<PopoverDrawerContent className="w-80" drawer={{ className: "max-h-[80dvh]" }}>A width such as w-80 therefore sizes the content, centered in a full-width
drawer, instead of shrinking the drawer to a 320px panel.
PopoverDrawerHeader and PopoverDrawerFooter take their spacing from that
box, as they do in the Popover.
The modal default
A Popover is non-modal by default; a Drawer is modal. With no modal prop,
each side keeps its own default: an anchored popover that closes on an outside
click at 768px and up, a drawer with an overlay below. Set modal to apply
it to both sides, or drawer={{ modal }} to change only the drawer:
At 768px and up: the Popover keeps its non-modal default.
<PopoverDrawer drawer={{ modal: false }}>The footer part
The shadcn Popover has no footer, but a drawer often needs an action row.
PopoverDrawerFooter is optional: a flex justify-end gap-2 row at 768px
and up, your DrawerFooter below.
To close from a button inside the content, control open, as you would with
the Popover:
At 768px and up: Apply sits at the end of a row and closes the Popover.
const [open, setOpen] = React.useState(false)
<PopoverDrawer open={open} onOpenChange={setOpen}>
<PopoverDrawerTrigger>Filters</PopoverDrawerTrigger>
<PopoverDrawerContent>
{/* … */}
<PopoverDrawerFooter>
<button type="button" onClick={() => setOpen(false)}>
Apply
</button>
</PopoverDrawerFooter>
</PopoverDrawerContent>
</PopoverDrawer>Desktop-only props
These props apply at 768px and up and are dropped below, because a drawer has no equivalent:
- Positioning on the Content:
side,align,sideOffset,alignOffset, and on Radix the rest of the Popper props (avoidCollisions,collisionPadding,sticky, …). - Hover on the Trigger:
openOnHover,delay,closeDelay. - Detached triggers on Base UI:
handle,triggerIdanddefaultTriggerIdon the root,handleandpayloadon the Trigger. Below 768px, the drawer opens only from aPopoverDrawerTriggerplaced insidePopoverDrawer, or from a controlledopen. - Anchoring on Radix:
PopoverDrawerAnchorrenders yourPopoverAnchorat 768px and up. Below 768px it renders its child (withasChild) or a plaindiv.
Accessibility
Below 768px the content is a dialog, and a dialog needs a name. A popover
without a title gets none: add a PopoverDrawerTitle, visually hidden if the
design has no heading.
At 768px and up: the hidden title names the Popover.
<PopoverDrawerTitle className="sr-only">Filters</PopoverDrawerTitle>API reference
Each part takes every prop of its shadcn Popover counterpart, plus a
drawer prop that takes the props of the matching Drawer part, except
children: drawer on the Content takes your DrawerContent props, and so
on. Only the parts below leave out more.
PopoverDrawer
| Prop | Type | Default |
|---|---|---|
drawer | Omit<ComponentProps<typeof Drawer>, "children" | RootState> | - |
RootState stands for the keys that drawer leaves out because they belong
to the shared root: open and defaultOpen, since both sides share one open
state, and the detached-trigger props handle, triggerId and
defaultTriggerId. drawer.onOpenChange runs after the shared
onOpenChange and receives your Drawer's own arguments.
PopoverDrawerTrigger
| Prop | Type | Default |
|---|---|---|
drawer | Omit<ComponentProps<typeof DrawerTrigger>, "children" | "handle" | "payload"> | - |
PopoverDrawerFooter
Takes every prop of a div, plus drawer with your DrawerFooter
props, except children.
PopoverDrawerAnchor
Radix only. Takes every prop of your PopoverAnchor and adds none.
Radix notes
drawer={{...}}is typed on Vaul:direction,dismissible,snapPoints, and so on.- Like Radix Popover, the drawer moves focus inside when it opens. Pass
drawer={{ autoFocus: false }}to keep Vaul's own default. - A non-modal Vaul drawer stays open when you tap outside it, unlike the
non-modal Popover. Control
opento close it from inside. - Vaul, the drawer library under the Radix Drawer, is no longer maintained. It still works; the Base UI Drawer is the maintained option.
Learn more
The underlying components are documented upstream: shadcn Popover and Drawer, built on Base UI Popover and Base UI Drawer, or on Radix Popover and Vaul.