bx

Drawer

A drawer component that slides in from the edge of the screen.

tsx
<Drawer>
  <DrawerTrigger for="confirm"><Button variant="outline">Open</Button></DrawerTrigger>
  <DrawerContent id="confirm">
    <DrawerHeader>
      <DrawerTitle>Are you sure?</DrawerTitle>
      <DrawerDescription>This will move the item to trash.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter className="mt-4">
      <DrawerClose><Button variant="outline">Cancel</Button></DrawerClose>
      <DrawerClose><Button>Move to trash</Button></DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Drawer is a mobile-friendly bottom sheet with a drag handle. Unlike the Sheet (desktop, slides in from an edge), the Drawer is optimized for mobile: it rises from the bottom. The handle at the top is a visual cue only: there is no swipe gesture, the drawer closes with a DrawerClose button, a tap on the backdrop or Escape.

For a form or an action flow aimed at mobile, the Drawer feels more natural than a Dialog that takes the whole screen or a Sheet that slides in from the edge.

Installation

Terminal
$ bext ui add drawer

Usage

tsx
import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, DrawerClose, Button } from "@bext-stack/ui/primitives"

<Drawer>
  <DrawerTrigger for="confirm">
    <Button variant="outline">Open</Button>
  </DrawerTrigger>
  <DrawerContent id="confirm">
    <DrawerHeader>
      <DrawerTitle>Are you sure?</DrawerTitle>
      <DrawerDescription>This will move the item to trash.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <DrawerClose><Button variant="outline">Cancel</Button></DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Anatomy

<Drawer>
  ├── <DrawerTrigger for="drawer-id">
  │   └── <Button>Open</Button>
  └── <DrawerContent id="drawer-id">
      ├── <DrawerHeader>
      │   ├── <DrawerTitle>...</DrawerTitle>
      │   └── <DrawerDescription>...</DrawerDescription>
      ├── ...free-form content...
      └── <DrawerFooter>
          └── <DrawerClose><Button>Cancel</Button></DrawerClose>

API Reference

DrawerTrigger

Prop Type Default Description
for string — Matches the id of the Content.

DrawerContent

Prop Type Default Description
id string — Matches Trigger.for.
ariaLabel string — Accessible name (aria-label) for a drawer that has no DrawerTitle.
className string — Default: fixed inset-x-0 bottom-0, bg-background rounded-t-lg border-t, with a handle at the top.

DrawerHeader / DrawerFooter

Prop Type Default Description
className string — Same layout as DialogHeader and DialogFooter.

DrawerTitle

Prop Type Default Description
id string — Optional id. Without it the island generates one to build aria-labelledby.

DrawerDescription

Prop Type Default Description
id string — Optional id. Without it the island generates one to build aria-describedby.

DrawerClose

Prop Type Default Description
children ReactNode — Wraps a button with data-ui="close". The drawer has no corner close button, so include one.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="open" Trigger data-target="#id" Clicking opens the drawer and locks page scroll.
data-state Trigger "open" | "closed" Set by the island, together with aria-haspopup="dialog", aria-expanded and aria-controls on the button inside.
data-overlay Content (outer element) hidden by default Modal overlay: the island toggles its hidden attribute.
data-ui="close" DrawerClose — Closes the drawer and returns focus to the trigger.
data-ui="overlay-bg" backdrop — Clicking the backdrop closes the drawer.

Keyboard

Key Action
Enter / Space On the trigger: opens the drawer and moves focus to its first focusable element, or to the panel when it has none.
Tab / Shift+Tab Moves through the focusable elements of the drawer and wraps around. Focus cannot leave the drawer while it is open.
Escape Closes the drawer and returns focus to the trigger.

Accessibility

The panel has role="dialog" aria-modal="true". The island points aria-labelledby at the DrawerTitle and aria-describedby at the DrawerDescription. A drawer without a DrawerTitle is named by its trigger button, or by the ariaLabel prop.

On open, focus moves to the first focusable element, or to the panel itself when the content has none. Tab and Shift+Tab are trapped inside. Escape, a DrawerClose button or a backdrop click closes the drawer, and focus returns to the trigger.

The handle is decorative and cannot be dragged. Always include a visible DrawerClose button: touch users without a keyboard otherwise depend on the backdrop.

Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.