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
triggerReactNodeElement that opens the drawer
triggerAsChildbooleantrueRender trigger as child
triggerClassNamestringTrigger class override
titleReactNodeDrawer title content
titleClassNamestringClass for DrawerTitle
descriptionReactNodeDrawer description content
descriptionClassNamestringClass for DrawerDescription
showHeaderbooleantrueShow/hide header block
headerClassNamestringClass for header container
headerContentReactNodeCustom header content (overrides title/description)
showFooterbooleanfalseShow/hide footer block
footerContentReactNodeFooter action content
footerClassNamestringClass for footer container
contentClassNamestringAdditional class for drawer content root
scrollAreaClassNamestringClass for internal ScrollArea
childrenClassNamestringClass for body wrapper inside scroll area
openbooleanControlled open state
onOpenChange(open: boolean) => voidOpen state callback
defaultOpenbooleanUncontrolled initial open state
shouldScaleBackgroundbooleantrueScale background while open
showClosebooleanfalseRender a screen-reader close button
closeClassNamestringClass for close button
closeLabelReactNode"Close"Accessible close label
classNamestringClass 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