Drawer

A panel that slides in from any edge of the screen. Built on Radix Dialog for focus trap, scroll lock and a11y; tuned for mobile with full-bleed side panels, a bottom-sheet grab handle, and swipe-to-dismiss.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add drawer

Usage#

usage.tsxTSX
import {  Drawer, DrawerTrigger, DrawerContent, DrawerHeader,  DrawerBody, DrawerFooter, DrawerTitle, DrawerDescription, DrawerClose,} from "@/components/ui/drawer"import { Button } from "@/components/ui/button"export function Example() {  return (    <Drawer>      <DrawerTrigger asChild>        <Button>Open drawer</Button>      </DrawerTrigger>      <DrawerContent side="right">        <DrawerHeader>          <DrawerTitle>Notifications</DrawerTitle>          <DrawerDescription>You're all caught up.</DrawerDescription>        </DrawerHeader>        <DrawerBody>{/* … */}</DrawerBody>        <DrawerFooter>          <DrawerClose asChild>            <Button variant="ghost">Close</Button>          </DrawerClose>          <Button>Mark all read</Button>        </DrawerFooter>      </DrawerContent>    </Drawer>  )}

Anatomy#

import {  Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription,  DrawerBody, DrawerFooter, DrawerClose, DrawerNav, DrawerView, DrawerNavBack, DrawerNavTrigger,} from "@/components/ui/drawer"<Drawer>  <DrawerTrigger />  <DrawerContent>    <DrawerHeader>      <DrawerTitle />      <DrawerDescription />    </DrawerHeader>    <DrawerBody />    <DrawerFooter>      <DrawerClose />    </DrawerFooter>  </DrawerContent></Drawer>{/* With in-sheet navigation */}<DrawerContent>  <DrawerNav>    <DrawerView>      <DrawerHeader>        <DrawerNavBack />        <DrawerTitle />      </DrawerHeader>      <DrawerBody>        <DrawerNavTrigger />      </DrawerBody>    </DrawerView>  </DrawerNav></DrawerContent>

Sides#

side on DrawerContent picks the edge the panel slides from: top, right (default), bottom, or left. Top and bottom render as sheets with a grab handle; left and right as full-height panels. Every variant animates with the dedicated ease-drawer curve.

Mobile: bottom sheet#

On touch devices the bottom sheet is the native pattern. It rounds its top corners, shows a grab handle, and pads past the home-indicator safe area. It is swipe-to-dismiss by default. Drag the handle or header down and release past ~45% (or flick) to close, handing the exit animation back to Radix. The drag only arms after a few pixels, so taps on the rows still register, and it never starts on the scrollable body, so a drag and a scroll never fight. Disable it with swipeToClose={false}.

A bottom sheet often needs to drill into nested views (an action sheet that opens a sub-panel, a settings menu, a multi-step filter) without ever leaving the sheet. Wrap the pages in DrawerNav and give each a DrawerView: only the active view renders, it enters with a native horizontal push (from the right going forward, the left on back), and the sheet's height animates to fit it. A DrawerNavTrigger drills in; a DrawerNavBack (which renders nothing at the root, so you can place it in a shared header) pops. The stack resets when the sheet closes, and swipe-down still dismisses the whole sheet from any view.

Form panel#

A right-side panel is the desktop home for an edit form; it keeps the underlying page visible for context. Compose labelled Input parts in the DrawerBody and land focus on the first field with onOpenAutoFocus.

A left drawer makes a compact, off-canvas navigation menu for small screens. The DrawerBody scrolls independently while the header and footer stay pinned; each link is wrapped in DrawerClose so picking one dismisses the panel.

Sizes#

size caps the panel's cross-axis extent: the width of left/right panels and the height of top/bottom sheets. Panels stay full-bleed below the cap, so on a phone they fill the screen and on desktop they settle into a panel. Four steps: sm, md (default), lg, xl.

Density#

Drawers are compact unless a DensityProvider or density="comfortable" opens them up: 24px padding and an 18px title instead of 16px and 16px. The content re-provides its density to the header, body and footer so every part stays in sync. Best driven once via DensityProvider. See Density.

API reference#

Drawer

The root, Radix Dialog.Root; renders no DOM. Every part that renders an element accepts className, merged last.

PropTypeDefaultDescription
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void-Called when the drawer opens or closes, including by a swipe.
modalbooleantrueTrap focus, lock scroll and hide the rest of the page from screen readers while open.

DrawerTrigger

Opens the drawer, Radix Dialog.Trigger. Renders a <button> and forwards all its props.

PropTypeDefaultDescription
asChildbooleanfalseMerge onto your own element (a Button) instead of rendering a <button>.
Data attributeDescription
data-state"open" or "closed".

DrawerContent

The panel, Radix Dialog.Content portalled to the body with its own scrim; forwards all its props. Re-provides its density to every part inside.

PropTypeDefaultDescription
side"top" | "right" | "bottom" | "left""right"The edge it slides from. Top and bottom are sheets with a grab handle; left and right are full-height panels.
size"sm" | "md" | "lg" | "xl""md"Caps the width of a side panel (max-w-sm to max-w-xl) or the height of a sheet (40 to 92dvh).
density"comfortable" | "compact""compact"24px padding with an 18px title, or 16px with a 16px title. Falls back to the nearest DensityProvider.
swipeToClosebooleantrueDrag the handle or header toward the edge to dismiss: past about 45% of the panel, or on a flick.
showHandlebooleantrueShow the grab handle on top and bottom sheets.
showClosebooleantrueRender the built-in top-right close button.
closeLabelstring"Close"Accessible label of the built-in close button.
onOpenAutoFocus(event: Event) => void-Called as focus moves in on open; preventDefault() and focus your first field instead.
onCloseAutoFocus(event: Event) => void-Called as focus returns to the trigger on close.
onEscapeKeyDown(event: KeyboardEvent) => void-Called on Escape; preventDefault() keeps the drawer open.
onInteractOutside(event: Event) => void-Called on a pointer down or focus outside the panel; preventDefault() keeps it open.
Data attributeDescription
data-state"open" or "closed", on the panel and on the scrim.
data-sideThe resolved side.
CSS variableDescription
--surfaceSet to var(--popover), read by nested Inputs and Selects.

DrawerHeader

The title block at the panel's edge, and a drag region for swipe-to-dismiss. Forwards all native <div> props.

DrawerTitle

The heading the drawer is announced by, Radix Dialog.Title (an <h2>). Required for accessibility; hide it with sr-only when the design has none.

DrawerDescription

Muted supporting copy, Radix Dialog.Description (a <p>).

DrawerBody

The region between header and footer that scrolls on its own; a swipe never starts here. Forwards all native <div> props.

DrawerFooter

The action row pinned to the panel's far edge: stacked and reversed on mobile, right-aligned from sm. Forwards all native <div> props.

DrawerClose

Closes the drawer, Radix Dialog.Close. Renders a <button> and forwards all its props.

PropTypeDefaultDescription
asChildbooleanfalseMerge onto your own element (a footer Button, a nav link) instead of rendering a <button>.

DrawerNav

An in-sheet navigation stack holding DrawerView pages; its height animates to the active one and it resets each time the drawer opens. Forwards all native <div> props except onChange.

PropTypeDefaultDescription
defaultView*string-The view shown when the drawer opens.
onViewChange(view: string) => void-Called whenever the active view changes, on a push or a pop.

DrawerView

One page in the stack, rendered only while active; it pushes in from the right going forward and from the left on back. Forwards all native <div> props.

PropTypeDefaultDescription
view*string-The id this page is shown for.
Data attributeDescription
data-direction"forward" or "back" after the first navigation; absent on the first view.

DrawerNavTrigger

Pushes a view onto the stack when clicked, unless your onClick calls preventDefault(). Forwards all native <button> props.

PropTypeDefaultDescription
view*string-The DrawerView to open.
asChildbooleanfalseMerge onto your own row or button instead of rendering a <button>.

DrawerNavBack

An icon button that pops the stack; renders nothing at the root view, so it can sit in a shared header. Forwards all native <button> props.

PropTypeDefaultDescription
labelstring"Back"Accessible label.
childrenReact.ReactNode-Replaces the default caret glyph.

useDrawerNav

A hook for driving the stack from anywhere inside a DrawerNav, e.g. after an async action. Returns { activeView, canGoBack, navigate(view), back() }.

FAQ#

Both are built on Radix Dialog, but reach for a Drawer when the panel should slide in from a screen edge: a right-side edit form that keeps the page visible, a left off-canvas nav, or a mobile bottom sheet. Use Dialog for a centered, modal confirmation or focused task that does not belong to an edge.