apps/docs/content/docs/react/migration/(components)/drawer.mdx
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>
);
}
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.
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.
Drawer.Handle component for visual drag indicatorDrawer.CloseTrigger renders a close buttonslot="close" automatically close the drawer| 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 |
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()
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
</>
```
<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>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Drawer isOpen={isOpen} onOpenChange={onOpenChange} placement="left"> <DrawerContent> {(onClose) => ( <> <DrawerHeader>Left Drawer</DrawerHeader> <DrawerBody>Content</DrawerBody> </> )} </DrawerContent> </Drawer>
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Drawer isOpen={isOpen} onOpenChange={onOpenChange} backdrop="blur"> <DrawerContent> {(onClose) => ( <> <DrawerHeader>Blurred Backdrop</DrawerHeader> <DrawerBody>Content</DrawerBody> </> )} </DrawerContent> </Drawer>
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx 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>
</>
```
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>
</>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <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>
</Tab>
<Tab value="v3">
tsx <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>
</Tab>
</Tabs>
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",
}}
/>
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>
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.Footer
useDisclosure + onPress → built-in trigger (first child of Drawer)useDisclosure → useOverlayState with open(), close(), toggle() methodsDrawer → prop on Drawer.Content. Default changed from "right" to "bottom"backdrop prop → Drawer.Backdrop variant prophideCloseButton/closeButton props → omit or customize Drawer.CloseTriggerslot="close" automatically close the drawerDrawer.Handle, velocity-based dismissalmotionProps (Framer Motion) → CSS-based animationssize, radius → use Tailwind CSSclassName on individual compound components