Back to Heroui

Drawer

apps/docs/content/docs/react/migration/(components)/drawer.mdx

3.0.512.4 KB
Original Source
<Callout type="info"> Refer to the [v3 Drawer documentation](/docs/react/components/drawer) for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2. </Callout>

Structure Changes

In v2, Drawer shared the same API as Modal, using DrawerContent, DrawerHeader, DrawerBody, and DrawerFooter with a render callback pattern:

tsx
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:

tsx
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.Handle component for visual drag indicator
  • Built-in close trigger: Drawer.CloseTrigger renders a close button
  • Slot-based close: Buttons with slot="close" automatically close the drawer

4. Prop Changes

v2 Propv3 EquivalentNotes
isOpenDrawer.Backdrop isOpenOr use useOverlayState
onOpenChangeDrawer.Backdrop onOpenChangeOr use useOverlayState
onClose-Use onOpenChange or slot="close" on buttons
placementDrawer.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)
backdropDrawer.Backdrop variantSame values: "opaque", "blur", "transparent"
isDismissableDrawer.Backdrop isDismissableSame
isKeyboardDismissDisabledDrawer.Backdrop isKeyboardDismissDisabledSame
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)

tsx
// v2
const { isOpen, onOpen, onOpenChange } = useDisclosure();

// v3
const state = useOverlayState();
// state.isOpen, state.open(), state.close(), state.toggle()

Migration Examples

Basic Drawer

<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>
</>
```
</Tab> <Tab value="v3"> ```tsx 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>
```
</Tab> </Tabs>

Placement

<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>

Backdrop Variant

<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>

Controlled State

<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>
</>
```
</Tab> <Tab value="v3"> ```tsx 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>
</>
```
</Tab> </Tabs>

Non-Dismissable

<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>

Styling Changes

v2: classNames Prop

tsx
<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

tsx
<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.Footer

Summary

  1. Component Structure: Render callback pattern → compound components with explicit subcomponents
  2. Trigger Pattern: External useDisclosure + onPress → built-in trigger (first child of Drawer)
  3. State Hook: useDisclosureuseOverlayState with open(), close(), toggle() methods
  4. Placement: Prop on Drawer → prop on Drawer.Content. Default changed from "right" to "bottom"
  5. Backdrop: backdrop prop → Drawer.Backdrop variant prop
  6. Close Button: hideCloseButton/closeButton props → omit or customize Drawer.CloseTrigger
  7. Slot-Based Close: Buttons with slot="close" automatically close the drawer
  8. New Features: Drag-to-dismiss with Drawer.Handle, velocity-based dismissal
  9. Animations: motionProps (Framer Motion) → CSS-based animations
  10. Styling Props Removed: size, radius → use Tailwind CSS
  11. ClassNames Removed: Use className on individual compound components