Skip to main contentSkip to docs navigation

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.

Drawer

Drawer body content goes here.

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

PlacementEdge
"start"Left (LTR) / Right (RTL)
"end"Right (LTR) / Left (RTL)
"top"Top
"bottom"Bottom

Start drawer

Slides in from the left (LTR).

JSX
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>
    </>
  )
}

End drawer

Slides in from the right (LTR).

JSX
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>
    </>
  )
}

Top drawer

Slides down from the top.

JSX
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>
    </>
  )
}

Bottom drawer

Slides up from the bottom.

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

Scrolling, no backdrop

Body scroll is enabled and the backdrop is removed.

Scrolling with backdrop

Body scroll is enabled and the backdrop remains visible.

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

Static backdrop

Clicking outside nudges this drawer rather than closing it.

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

Sheet drawer

Flush against the viewport edge — no inset, rounding, or border.

Translucent drawer

Frosted-glass background over the page content.

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

Fullscreen drawer

Fills the full viewport inset area.

JSX
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>
    </>
  )
}

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

Drawer with stacked actions

A drawer with multiple footer actions, stacking full-width at the sm breakpoint.

JSX
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

PropTypeDefault
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?booleantrue
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?booleanfalse
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

PropTypeDefault
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

PropTypeDefault
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

PropTypeDefault
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

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