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.

Installation

npx shadcn@latest add @alexandrejoly/popover-drawer

Update 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.

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.

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.

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, triggerId and defaultTriggerId on the root, handle and payload on the Trigger. Below 768px, the drawer opens only from a PopoverDrawerTrigger placed inside PopoverDrawer, or from a controlled open.
  • Anchoring on Radix: PopoverDrawerAnchor renders your PopoverAnchor at 768px and up. Below 768px it renders its child (with asChild) or a plain div.

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.

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

PropTypeDefault
drawerOmit<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

PropTypeDefault
drawerOmit<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 open to 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.

On this page