Drawer Wrapper
DrawerWrapper provides a convenient API to compose mobile-first bottom sheets and drawers without wiring Radix primitives manually.
It includes trigger handling, optional header/footer regions, scrollable body content, and controlled/uncontrolled open state.
Installation
1. Install dependencies:
npm install vaul2. Install the Drawer primitive via shadcn:
npx shadcn@latest add drawer3. Copy the wrapper source:
- Source in this monorepo:
packages/ui/src/common/overlays/drawer-wrapper.tsx - Place in your project:
components/common/overlays/drawer-wrapper.tsx - Required components:
components/ui/drawer.tsx,components/ui/scroll-area.tsx,lib/utils.ts
4. Update import paths to match your project structure.
Usage
import { DrawerWrapper } from "@hoag/ui/drawer-wrapper";
import { Button } from "@hoag/ui/components/button";
export default function Demo() {
return (
<DrawerWrapper
trigger={<Button>Open Drawer</Button>}
title="Profile Settings"
description="Manage your account preferences"
>
<div>Drawer content goes here.</div>
</DrawerWrapper>
);
}Default
<DrawerWrapper
trigger={<Button variant="outline">Open Drawer</Button>}
title="Update Profile"
description="Make changes to your profile information."
>
<div className="space-y-2 py-2">
<p className="text-sm text-muted-foreground">Name: Hoag Vu</p>
<p className="text-sm text-muted-foreground">Email: team@hoag.dev</p>
</div>
</DrawerWrapper>With Footer Actions
<DrawerWrapper
trigger={<Button>Open with Footer</Button>}
title="Confirm Changes"
showFooter
footerContent={
<div className="flex w-full gap-2">
<Button className="flex-1">Save</Button>
<Button variant="outline" className="flex-1">
Cancel
</Button>
</div>
}
>
<p>Your modifications are ready.</p>
</DrawerWrapper>Custom Header Content
Use headerContent when you need custom layout/actions in the header area.
<DrawerWrapper
trigger={<Button>Advanced Header</Button>}
headerContent={<CustomHeader />}
>
<div>Body content</div>
</DrawerWrapper>Minimal Mode
Hide the default header and use the drawer as a clean container.
<DrawerWrapper trigger={<Button>Minimal</Button>} showHeader={false}>
<div>No header is rendered.</div>
</DrawerWrapper>Controlled Mode
Use controlled mode when the open state is managed by parent logic.
import { useState } from "react";
export default function ControlledDrawer() {
const [open, setOpen] = useState(false);
return (
<DrawerWrapper
open={open}
onOpenChange={setOpen}
trigger={<Button variant="outline">Open Controlled</Button>}
title="Controlled Drawer"
description="State is managed by parent"
showFooter
footerContent={<Button onClick={() => setOpen(false)}>Close</Button>}
>
<p className="text-sm text-muted-foreground">
You can close this from parent state.
</p>
</DrawerWrapper>
);
}Tips
- Keep long content inside the default scroll region (
ScrollArea) for stable height. - Use
childrenClassNameto control body spacing without overriding container layout. - Use
shouldScaleBackground={false}if background scaling conflicts with your page motion. - Use
showCloseto render a screen-reader close control for accessibility flows.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactNode | — | Element that opens the drawer |
triggerAsChild | boolean | true | Render trigger as child |
triggerClassName | string | — | Trigger class override |
title | ReactNode | — | Drawer title content |
titleClassName | string | — | Class for DrawerTitle |
description | ReactNode | — | Drawer description content |
descriptionClassName | string | — | Class for DrawerDescription |
showHeader | boolean | true | Show/hide header block |
headerClassName | string | — | Class for header container |
headerContent | ReactNode | — | Custom header content (overrides title/description) |
showFooter | boolean | false | Show/hide footer block |
footerContent | ReactNode | — | Footer action content |
footerClassName | string | — | Class for footer container |
contentClassName | string | — | Additional class for drawer content root |
scrollAreaClassName | string | — | Class for internal ScrollArea |
childrenClassName | string | — | Class for body wrapper inside scroll area |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Open state callback |
defaultOpen | boolean | — | Uncontrolled initial open state |
shouldScaleBackground | boolean | true | Scale background while open |
showClose | boolean | false | Render a screen-reader close button |
closeClassName | string | — | Class for close button |
closeLabel | ReactNode | "Close" | Accessible close label |
className | string | — | Class passed to wrapper content root |
Inherited DrawerContent Props
DrawerWrapper also forwards most DrawerContent props (except title), such as event handlers and portal/content configuration.