Drawer
freeDrawer brings supporting panels into the current page without a route change.
@floating-ui/reactmotion
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function UsageDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer title="Drawer Title" isOpen={open} onClose={() => setOpen(false)}>
<p className="text-muted-foreground">
Render any content here — forms, details, filters, or supplementary
actions.
</p>
</Drawer>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add DrawerExamples
Basic
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function BasicDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer title="Drawer Title" isOpen={open} onClose={() => setOpen(false)}>
<p className="text-muted-foreground">
Render any content here — forms, details, filters, or supplementary
actions.
</p>
</Drawer>
</div>
);
}Placement
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
import Radio from '@/components/ui/Radio';
type Placement = 'top' | 'right' | 'bottom' | 'left';
const placements: { label: string; value: Placement }[] = [
{ label: 'Top', value: 'top' },
{ label: 'Right', value: 'right' },
{ label: 'Bottom', value: 'bottom' },
{ label: 'Left', value: 'left' },
];
export default function PlacementDemo() {
const [open, setOpen] = useState(false);
const [placement, setPlacement] = useState<Placement>('right');
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<Radio.Group value={placement} onChange={(value) => setPlacement(value)}>
{placements.map((item) => (
<Radio key={item.value} value={item.value}>
{item.label}
</Radio>
))}
</Radio.Group>
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer
title="Drawer Title"
isOpen={open}
placement={placement}
onClose={() => setOpen(false)}
>
<p className="text-muted-foreground">
Drag the handle to dismiss this drawer when it opens from the top or
bottom edge.
</p>
</Drawer>
</div>
);
}Width & Height
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function WidthHeightDemo() {
const [wideOpen, setWideOpen] = useState(false);
const [tallOpen, setTallOpen] = useState(false);
return (
<div className="flex flex-wrap justify-center gap-2">
<Button onClick={() => setWideOpen(true)}>Wide Drawer</Button>
<Button onClick={() => setTallOpen(true)}>Tall Drawer</Button>
<Drawer
title="Wide Drawer"
isOpen={wideOpen}
placement="right"
width={600}
onClose={() => setWideOpen(false)}
>
<p className="text-muted-foreground">
This drawer sets <code>width</code> to 600px.
</p>
</Drawer>
<Drawer
title="Tall Drawer"
isOpen={tallOpen}
placement="bottom"
height={320}
onClose={() => setTallOpen(false)}
>
<p className="text-muted-foreground">
This drawer sets <code>height</code> to 320px.
</p>
</Drawer>
</div>
);
}Footer
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function FooterDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer
title="Drawer Title"
isOpen={open}
onClose={() => setOpen(false)}
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="solid" onClick={() => setOpen(false)}>
Confirm
</Button>
</div>
}
>
<p className="text-muted-foreground">
The footer stays pinned while this content area scrolls.
</p>
</Drawer>
</div>
);
}Custom Style
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function CustomStyleDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer
title="Drawer Title"
isOpen={open}
onClose={() => setOpen(false)}
headerClass="bg-muted"
bodyClass="bg-muted/40"
footerClass="border-t-0"
contentClassName="m-0"
footer={
<div className="flex w-full gap-2">
<Button block onClick={() => setOpen(false)}>
Cancel
</Button>
<Button block variant="solid" onClick={() => setOpen(false)}>
Confirm
</Button>
</div>
}
>
<p className="text-muted-foreground">
The header, body, and footer regions each take their own class name.
</p>
</Drawer>
</div>
);
}Closable
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function ClosableDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer
title="Drawer Title"
isOpen={open}
closable={false}
onClose={() => setOpen(false)}
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="solid" onClick={() => setOpen(false)}>
Confirm
</Button>
</div>
}
>
<p className="text-muted-foreground">
With the close button hidden, the footer actions are the only way to
dismiss the drawer.
</p>
</Drawer>
</div>
);
}Custom Backdrop
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
export default function BackdropDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Open Drawer
</Button>
<Drawer
title="Drawer Title"
isOpen={open}
overlayClassName="bg-transparent hidden"
onClose={() => setOpen(false)}
/>
</div>
);
}Form
Preview
Dark
import { useState } from 'react';
import Button from '@/components/ui/Button';
import Drawer from '@/components/ui/Drawer';
import Form from '@/components/ui/Form';
import Input from '@/components/ui/Input';
export default function FormDemo() {
const [open, setOpen] = useState(false);
return (
<div className="flex justify-center">
<Button variant="solid" onClick={() => setOpen(true)}>
Edit Profile
</Button>
<Drawer
title="Edit Profile"
isOpen={open}
onClose={() => setOpen(false)}
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="solid" type="submit" form="drawer-profile-form">
Save
</Button>
</div>
}
>
<Form
id="drawer-profile-form"
onSubmit={(event) => {
event.preventDefault();
setOpen(false);
}}
>
<Form.Field label="Full name" htmlFor="drawer-profile-name">
<Input id="drawer-profile-name" defaultValue="Jordan Blake" />
</Form.Field>
<Form.Field label="Email" htmlFor="drawer-profile-email">
<Input
id="drawer-profile-email"
defaultValue="jordan@example.com"
/>
</Form.Field>
</Form>
</Drawer>
</div>
);
}API
Drawer accepts the props below plus native props for the drawer content div.
| Prop | Description | Type | Default |
|---|---|---|---|
bodyClass | Extra class names applied to the scrollable body region. | string | - |
children | Content rendered in the scrollable body of the drawer. | ReactNode | - |
closable | Shows the default close button in the header. | boolean | true |
contentClassName | Class names applied to the drawer content surface. | string | - |
footer | Content rendered in a footer region below the body. | string | ReactNode | - |
footerClass | Extra class names applied to the footer region. | string | - |
headerClass | Extra class names applied to the header region. | string | - |
height | Height of the drawer. Used when placement is 'top' or 'bottom'. | string | number | 370 |
isOpen | Controls whether the drawer is visible. | boolean | false |
lockScroll | Locks document scrolling through the overlay while the drawer is mounted. | boolean | true |
onClose | Called when the drawer asks to close through the close button, overlay, Escape key, or a drag-to-dismiss gesture. | DrawerOpenChangeHandler | - |
onOpen | Called when the floating layer reports that the drawer opened. | DrawerOpenChangeHandler | - |
overlayClassName | Class names applied to the overlay element. | string | - |
placement | Edge the drawer slides out from. 'top' and 'bottom' add a drag handle for dismissing the drawer by swipe. | DrawerPlacement | 'right' |
ref | Ref forwarded to the drawer content element. | Ref<HTMLDivElement> | - |
shouldCloseOnEsc | Allows Escape to request closing the drawer. | boolean | true |
shouldCloseOnOverlayClick | Allows an overlay click to request closing the drawer. | boolean | true |
title | Title rendered in the header region. | string | ReactNode | - |
width | Width of the drawer. Used when placement is 'left' or 'right'. | number | 370 |
...nativeDivProps | Native props applied to the drawer content element. | NativeDrawerDivProps | - |
Types
type DrawerPlacement = 'top' | 'right' | 'bottom' | 'left';
type DrawerOpenChangeHandler = (open: boolean) => void;
type NativeDrawerDivProps = Omit<ComponentProps<'div'>, 'title'>;