Drawer
Slide-in panel anchored to a viewport edge, for navigation, filters, and shopping carts.
Examples
Live demo
The button below opens a drawer anchored to the viewport's start edge.
visible opens and closes the drawer. A close request (the Escape key, a backdrop click, a close button) fires onClose, and onVisibleChange(false) beside it, so visible={open} onVisibleChange={setOpen} works as well as onClose={() => setOpen(false)}. With defaultVisible instead, the drawer is open at first and closes itself.
import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Open drawer</Button>
<Drawer placement="start" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Drawer body content goes here.</p>
</DrawerBody>
</Drawer>
</>
)
}Placement
Always add a placement prop — the default off-screen transform without one is undefined.
| Placement | Edge |
|---|---|
"start" | Left (LTR) / Right (RTL) |
"end" | Right (LTR) / Left (RTL) |
"top" | Top |
"bottom" | Bottom |
import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Start</Button>
<Drawer placement="start" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Start drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Slides in from the left (LTR).</p>
</DrawerBody>
</Drawer>
</>
)
}import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>End</Button>
<Drawer placement="end" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>End drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Slides in from the right (LTR).</p>
</DrawerBody>
</Drawer>
</>
)
}import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Top</Button>
<Drawer placement="top" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Top drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Slides down from the top.</p>
</DrawerBody>
</Drawer>
</>
)
}import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Bottom</Button>
<Drawer placement="bottom" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Bottom drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Slides up from the bottom.</p>
</DrawerBody>
</Drawer>
</>
)
}Behaviors
Body scroll
By default, body scroll is locked while a drawer is open. Set scroll to allow the page to scroll while the drawer is open. Combine with backdrop={false} to remove the backdrop entirely.
import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visibleScrolling, setVisibleScrolling] = useState(false)
const [visibleScrollBackdrop, setVisibleScrollBackdrop] = useState(false)
return (
<>
<Button color="primary" onClick={() => setVisibleScrolling(true)}>
Scrolling, no backdrop
</Button>
<Button color="primary" onClick={() => setVisibleScrollBackdrop(true)}>
Scrolling with backdrop
</Button>
<Drawer
backdrop={false}
placement="start"
scroll
visible={visibleScrolling}
onClose={() => setVisibleScrolling(false)}
>
<DrawerHeader>
<DrawerTitle>Scrolling, no backdrop</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Body scroll is enabled and the backdrop is removed.</p>
</DrawerBody>
</Drawer>
<Drawer
placement="start"
scroll
visible={visibleScrollBackdrop}
onClose={() => setVisibleScrollBackdrop(false)}
>
<DrawerHeader>
<DrawerTitle>Scrolling with backdrop</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Body scroll is enabled and the backdrop remains visible.</p>
</DrawerBody>
</Drawer>
</>
)
}Static backdrop
Set backdrop="static" to prevent closing on backdrop click — the drawer nudges briefly to signal it requires explicit action. Add keyboard={false} to also disable Escape.
import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Static backdrop</Button>
<Drawer
backdrop="static"
placement="start"
visible={visible}
onClose={() => setVisible(false)}
>
<DrawerHeader>
<DrawerTitle>Static backdrop</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Clicking outside nudges this drawer rather than closing it.</p>
</DrawerBody>
</Drawer>
</>
)
}Animation
Drawers animate in and out by default. Add instant to skip all transitions.
Appearance
sheet removes the inset gap, border radius, and border — the panel sits flush against the viewport edge. translucent applies a frosted-glass background. Both combine freely with placement and behavior props.
import { useState } from 'react'
import {
Button,
Drawer,
DrawerBody,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@chassis-ui/react'
export const Example = () => {
const [visibleSheet, setVisibleSheet] = useState(false)
const [visibleTranslucent, setVisibleTranslucent] = useState(false)
return (
<>
<Button onClick={() => setVisibleSheet(true)}>Sheet</Button>
<Button onClick={() => setVisibleTranslucent(true)}>Translucent</Button>
<Drawer sheet placement="start" visible={visibleSheet} onClose={() => setVisibleSheet(false)}>
<DrawerHeader>
<DrawerTitle>Sheet drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Flush against the viewport edge — no inset, rounding, or border.</p>
</DrawerBody>
<DrawerFooter>
<Button color="neutral" onClick={() => setVisibleSheet(false)}>
Close
</Button>
</DrawerFooter>
</Drawer>
<Drawer
translucent
placement="start"
visible={visibleTranslucent}
onClose={() => setVisibleTranslucent(false)}
>
<DrawerHeader>
<DrawerTitle>Translucent drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Frosted-glass background over the page content.</p>
</DrawerBody>
<DrawerFooter>
<Button color="neutral" onClick={() => setVisibleTranslucent(false)}>
Close
</Button>
</DrawerFooter>
</Drawer>
</>
)
}Fullscreen
Add fullscreen to expand the panel to fill the viewport, animating in and out from the direction of its placement.
import { useState } from 'react'
import { Button, Drawer, DrawerBody, DrawerHeader, DrawerTitle } from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Fullscreen</Button>
<Drawer fullscreen placement="bottom" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Fullscreen drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>Fills the full viewport inset area.</p>
</DrawerBody>
</Drawer>
</>
)
}Footer layout
<DrawerFooter> uses flex-direction: row-reverse, so buttons are right-aligned without extra props — write the primary action first in source, it renders on the right. At the sm breakpoint, buttons stretch to equal widths. Add stacked to stack them as full-width columns instead.
import { useState } from 'react'
import {
Button,
Drawer,
DrawerBody,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@chassis-ui/react'
export const Example = () => {
const [visible, setVisible] = useState(false)
return (
<>
<Button onClick={() => setVisible(true)}>Open drawer</Button>
<Drawer placement="start" visible={visible} onClose={() => setVisible(false)}>
<DrawerHeader>
<DrawerTitle>Drawer with stacked actions</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>A drawer with multiple footer actions, stacking full-width at the sm breakpoint.</p>
</DrawerBody>
<DrawerFooter stacked>
<Button color="primary">Take action</Button>
<Button color="secondary" onClick={() => setVisible(false)}>
Cancel
</Button>
</DrawerFooter>
</Drawer>
</>
)
}Responsive
Set responsive to a breakpoint ('sm' | 'md' | 'lg' | 'xl' | '2xl') to render as a drawer only below that breakpoint — above it, the panel renders inline as a flex container instead (.drawer-header hidden, .drawer-body laid out as a row). This is the basis for navigation that collapses off-screen on narrow viewports and expands inline on wide ones, as used by Navbar's collapsible menu region. A drawer that is open when the viewport grows past the breakpoint asks to close, as on any other close request, so it doesn't stay over the page as a modal dialog.
Accessibility
The <dialog> element carries an implicit role="dialog" when opened with showModal(). <Drawer> is labelled by its <DrawerTitle> automatically — render one anywhere inside and aria-labelledby names it, under a generated id or the id you give the title, giving the drawer an accessible name with no manual wiring. A drawer with no title has no aria-labelledby: name it with aria-label, which also wins over a title, or set your own aria-labelledby on <Drawer> to point elsewhere.
Focus moves to the drawer on open — to the first element with data-autofocus (or an autofocus attribute in server-rendered HTML), otherwise to the drawer itself. React's autoFocus prop isn't enough in a page rendered in the browser: React writes no autofocus attribute there. After the drawer closes, focus returns to whichever element was focused when it opened. Opening a second drawer while one is already open automatically closes the first.
API
Drawer
| Prop | Type | Default |
|---|---|---|
backdrop? | boolean | "static" | true |
Show a backdrop while the drawer is open. 'static' blocks closing on backdrop click
(the drawer nudges instead). | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
defaultVisible? | boolean | — |
Whether the drawer is open when it first renders. Such a drawer is uncontrolled: it closes
itself on a close request. Use visible to open and close it from outside. | ||
fitContent? | boolean | — |
Size the height to the drawer's content instead of --drawer-height. Meaningful for
placement="bottom". | ||
fullscreen? | boolean | — |
Expand the panel to fill the viewport, animating in from the direction of placement. | ||
instant? | boolean | — |
| Disable the open/close transition entirely. | ||
keyboard? | boolean | true |
| Closes the drawer when the escape key is pressed. | ||
onClose? | (() => void) | — |
| Callback fired when the drawer requests to be closed (escape, backdrop click, close button, or another drawer opening). | ||
onClosePrevented? | (() => void) | — |
Callback fired when a close attempt is blocked (static backdrop click, or escape with keyboard=false). | ||
onHidden? | (() => void) | — |
| Callback fired after the exit transition completes and the drawer is fully hidden. | ||
onShow? | (() => void) | — |
| Callback fired when the drawer starts to open. | ||
onShown? | (() => void) | — |
| Callback fired after the entry transition completes and the drawer is fully visible. | ||
onVisibleChange? | ((visible: boolean) => void) | — |
Callback fired with false when the drawer requests to be closed, at the same moments as
onClose. It makes visible and onVisibleChange a pair that takes a state setter. | ||
open? | boolean | — |
Renders the drawer open from its first paint, server HTML included, as a non-modal dialog: a
static preview. It stays open until a close request. Use visible or defaultVisible to
open it as a modal dialog. | ||
placement | "start" | "end" | "top" | "bottom" | — |
| Which viewport edge the panel slides in from. Always required — there is no default off-screen transform without one. | ||
responsive? | "sm" | "md" | "lg" | "xl" | "2xl" | — |
| Renders as a drawer only below this breakpoint — inline as a flex container above it. | ||
scroll? | boolean | false |
| Allow the page behind the drawer to scroll while it's open. | ||
sheet? | boolean | — |
| Remove the inset gap, border radius, and border so the panel sits flush against the viewport edge. | ||
translucent? | boolean | — |
| Apply a frosted-glass background to the panel. | ||
visible? | boolean | — |
Whether the drawer is open. The drawer is then controlled: a close request only fires
onClose and onVisibleChange, and the drawer closes when this becomes false. | ||
DrawerBody
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |
DrawerFooter
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |
stacked? | boolean | — |
| Stack the footer actions as full-width columns instead of a right-aligned row. | ||
DrawerHeader
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
closeButton? | boolean | — |
| Add a close button component to the header. | ||
closeLabel? | string | — |
Overrides the close button's accessible name (defaults to 'Close'). Set this for
non-English UIs. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |
DrawerTitle
| Prop | Type | Default |
|---|---|---|
asChild? | boolean | — |
Render the single child element in place of this component's own element, with this
component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log
in</Link></Button>. The alternative to component for React Server Components — an element
can be passed from the server to this package's client components, a component reference
(component={Link}) can't. | ||
className? | string | — |
| A string of all className you want applied to the base component. | ||
component? | ElementType | — |
| Component used for the root node. Either a string to use a HTML element or a component. | ||
ref? | any | — |