Components
Overlay & Dialogs
Drawer

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 vaul

2. Install the Drawer primitive via shadcn:

npx shadcn@latest add drawer

3. Copy the wrapper source:

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 childrenClassName to control body spacing without overriding container layout.
  • Use shouldScaleBackground={false} if background scaling conflicts with your page motion.
  • Use showClose to render a screen-reader close control for accessibility flows.

API Reference

Props

PropTypeDefaultDescription
triggerReactNode—Element that opens the drawer
triggerAsChildbooleantrueRender trigger as child
triggerClassNamestring—Trigger class override
titleReactNode—Drawer title content
titleClassNamestring—Class for DrawerTitle
descriptionReactNode—Drawer description content
descriptionClassNamestring—Class for DrawerDescription
showHeaderbooleantrueShow/hide header block
headerClassNamestring—Class for header container
headerContentReactNode—Custom header content (overrides title/description)
showFooterbooleanfalseShow/hide footer block
footerContentReactNode—Footer action content
footerClassNamestring—Class for footer container
contentClassNamestring—Additional class for drawer content root
scrollAreaClassNamestring—Class for internal ScrollArea
childrenClassNamestring—Class for body wrapper inside scroll area
openboolean—Controlled open state
onOpenChange(open: boolean) => void—Open state callback
defaultOpenboolean—Uncontrolled initial open state
shouldScaleBackgroundbooleantrueScale background while open
showClosebooleanfalseRender a screen-reader close button
closeClassNamestring—Class for close button
closeLabelReactNode"Close"Accessible close label
classNamestring—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.


MIT 2026 © @hoag/ui