Drawer
Migration guide for Drawer from HeroUI v2 to v3
Refer to the v3 Drawer documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.
Structure Changes
In v2, Drawer shared the same API as Modal, using DrawerContent, DrawerHeader, DrawerBody, and DrawerFooter with a render callback pattern:
import { Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, Button, useDisclosure } from "@heroui/react";
export default function App() {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
return (
<>
<Button onPress={onOpen}>Open Drawer</Button>
<Drawer isOpen={isOpen} onOpenChange={onOpenChange} placement="right">
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Drawer Title</DrawerHeader>
<DrawerBody>
<p>Drawer content goes here.</p>
</DrawerBody>
<DrawerFooter>
<Button onPress={onClose}>Close</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
</Drawer>
</>
);
}In v3, Drawer uses a compound component pattern with explicit subcomponents and built-in trigger support:
import { Drawer, Button } from "@heroui/react";
export default function App() {
return (
<Drawer>
<Button>Open Drawer</Button>
<Drawer.Backdrop>
<Drawer.Content placement="right">
<Drawer.Dialog>
<Drawer.Handle />
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Drawer Title</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>
<p>Drawer content goes here.</p>
</Drawer.Body>
<Drawer.Footer>
<Button slot="close">Close</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
);
}Key Changes
1. Component Structure
v2: Drawer wrapping DrawerContent with a render callback pattern; separate trigger via useDisclosure
v3: Compound components: Drawer, Drawer.Backdrop, Drawer.Content, Drawer.Dialog, Drawer.Header, Drawer.Heading, Drawer.Body, Drawer.Footer, Drawer.Handle, Drawer.CloseTrigger. Trigger is the first child of Drawer.
2. Trigger Pattern
v2: External trigger using useDisclosure hook with isOpen/onOpenChange
v3: Built-in trigger — first child of Drawer becomes the trigger automatically. Controlled state available via useOverlayState hook.
3. New Features in v3
- Drag to dismiss: Built-in pointer-based drag gestures on handle, header, and footer areas
- Drag handle:
Drawer.Handlecomponent for visual drag indicator - Built-in close trigger:
Drawer.CloseTriggerrenders a close button - Slot-based close: Buttons with
slot="close"automatically close the drawer
4. Prop Changes
| v2 Prop | v3 Equivalent | Notes |
|---|---|---|
isOpen | Drawer.Backdrop isOpen | Or use useOverlayState |
onOpenChange | Drawer.Backdrop onOpenChange | Or use useOverlayState |
onClose | - | Use onOpenChange or slot="close" on buttons |
placement | Drawer.Content placement | "right" → "right", "left" → "left", "top" → "top", "bottom" → "bottom". Default changed from "right" to "bottom" |
size | - | Removed (use Tailwind CSS on Drawer.Dialog) |
radius | - | Removed (use Tailwind CSS) |
backdrop | Drawer.Backdrop variant | Same values: "opaque", "blur", "transparent" |
isDismissable | Drawer.Backdrop isDismissable | Same |
isKeyboardDismissDisabled | Drawer.Backdrop isKeyboardDismissDisabled | Same |
shouldBlockScroll | - | Always blocks scroll in v3 |
hideCloseButton | - | Omit Drawer.CloseTrigger to hide |
closeButton | - | Pass custom content to Drawer.CloseTrigger |
motionProps | - | Removed (CSS-based animations in v3) |
disableAnimation | - | Removed |
portalContainer | - | Removed |
classNames | - | Use className on individual compound components |
5. Hook Changes
v2: useDisclosure hook for open/close state
v3: useOverlayState hook (replaces useDisclosure)
// v2
const { isOpen, onOpen, onOpenChange } = useDisclosure();
// v3
const state = useOverlayState();
// state.isOpen, state.open(), state.close(), state.toggle()Migration Examples
Basic Drawer
import { Drawer, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, Button, useDisclosure } from "@heroui/react";
const { isOpen, onOpen, onOpenChange } = useDisclosure();
<>
<Button onPress={onOpen}>Open</Button>
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Title</DrawerHeader>
<DrawerBody>Content</DrawerBody>
<DrawerFooter>
<Button onPress={onClose}>Close</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
</Drawer>
</>import { Drawer, Button } from "@heroui/react";
<Drawer>
<Button>Open</Button>
<Drawer.Backdrop>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Title</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>Content</Drawer.Body>
<Drawer.Footer>
<Button slot="close">Close</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>Placement
<Drawer isOpen={isOpen} onOpenChange={onOpenChange} placement="left">
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Left Drawer</DrawerHeader>
<DrawerBody>Content</DrawerBody>
</>
)}
</DrawerContent>
</Drawer><Drawer>
<Button>Open</Button>
<Drawer.Backdrop>
<Drawer.Content placement="left">
<Drawer.Dialog>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Left Drawer</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>Content</Drawer.Body>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>Backdrop Variant
<Drawer isOpen={isOpen} onOpenChange={onOpenChange} backdrop="blur">
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Blurred Backdrop</DrawerHeader>
<DrawerBody>Content</DrawerBody>
</>
)}
</DrawerContent>
</Drawer><Drawer>
<Button>Open</Button>
<Drawer.Backdrop variant="blur">
<Drawer.Content>
<Drawer.Dialog>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Blurred Backdrop</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>Content</Drawer.Body>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>Controlled State
import { useDisclosure } from "@heroui/react";
const { isOpen, onOpen, onOpenChange } = useDisclosure();
<>
<Button onPress={onOpen}>Open</Button>
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Controlled</DrawerHeader>
<DrawerBody>Content</DrawerBody>
<DrawerFooter>
<Button onPress={onClose}>Close</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
</Drawer>
</>import { useOverlayState } from "@heroui/react";
const state = useOverlayState();
<>
<Button onPress={state.open}>Open</Button>
<Drawer state={state}>
<Drawer.Backdrop>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Heading>Controlled</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>Content</Drawer.Body>
<Drawer.Footer>
<Button onPress={state.close}>Close</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
</>Non-Dismissable
<Drawer
isOpen={isOpen}
onOpenChange={onOpenChange}
isDismissable={false}
hideCloseButton
>
<DrawerContent>
{(onClose) => (
<>
<DrawerHeader>Confirm Action</DrawerHeader>
<DrawerBody>Are you sure?</DrawerBody>
<DrawerFooter>
<Button onPress={onClose}>Confirm</Button>
</DrawerFooter>
</>
)}
</DrawerContent>
</Drawer><Drawer>
<Button>Open</Button>
<Drawer.Backdrop isDismissable={false}>
<Drawer.Content>
<Drawer.Dialog>
<Drawer.Header>
<Drawer.Heading>Confirm Action</Drawer.Heading>
</Drawer.Header>
<Drawer.Body>Are you sure?</Drawer.Body>
<Drawer.Footer>
<Button slot="close">Confirm</Button>
</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>Styling Changes
v2: classNames Prop
<Drawer
classNames={{
wrapper: "custom-wrapper",
base: "custom-base",
backdrop: "custom-backdrop",
header: "custom-header",
body: "custom-body",
footer: "custom-footer",
closeButton: "custom-close",
}}
/>v3: Direct className Props
<Drawer>
<Button>Open</Button>
<Drawer.Backdrop className="custom-backdrop">
<Drawer.Content>
<Drawer.Dialog className="custom-base">
<Drawer.CloseTrigger className="custom-close" />
<Drawer.Header className="custom-header">
<Drawer.Heading>Title</Drawer.Heading>
</Drawer.Header>
<Drawer.Body className="custom-body">Content</Drawer.Body>
<Drawer.Footer className="custom-footer">Actions</Drawer.Footer>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>Component Anatomy
The v3 Drawer follows this structure:
Drawer (Root)
├── [Trigger element] (first child becomes trigger)
└── Drawer.Backdrop
└── Drawer.Content (placement)
└── Drawer.Dialog
├── Drawer.Handle (optional, drag indicator)
├── Drawer.CloseTrigger (optional, close button)
├── Drawer.Header
│ └── Drawer.Heading
├── Drawer.Body (scrollable)
└── Drawer.FooterSummary
- Component Structure: Render callback pattern → compound components with explicit subcomponents
- Trigger Pattern: External
useDisclosure+onPress→ built-in trigger (first child ofDrawer) - State Hook:
useDisclosure→useOverlayStatewithopen(),close(),toggle()methods - Placement: Prop on
Drawer→ prop onDrawer.Content. Default changed from"right"to"bottom" - Backdrop:
backdropprop →Drawer.Backdropvariantprop - Close Button:
hideCloseButton/closeButtonprops → omit or customizeDrawer.CloseTrigger - Slot-Based Close: Buttons with
slot="close"automatically close the drawer - New Features: Drag-to-dismiss with
Drawer.Handle, velocity-based dismissal - Animations:
motionProps(Framer Motion) → CSS-based animations - Styling Props Removed:
size,radius→ use Tailwind CSS - ClassNames Removed: Use
classNameon individual compound components