Drawer
A drawer component that slides in from the edge of the screen.
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
$ bext ui add drawer
Copy and paste the following into src/components/ui/drawer.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="drawer" hooks.
Usage
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.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page