Drawer
A panel that slides in from the edge, for details and side tasks.
SourcePackage @weft-js/warpPlayground
Preview
tsx
import { state } from '@weft-js/core'import { Badge, Button, Drawer, Row, Stack, Text } from '@weft-js/warp'export function DeployDetails() { const open = state(false) const picked = state(DEPLOYS[0]!) const show = (d: typeof DEPLOYS[number]) => { picked.value = d; open.value = true } return ( <Stack gap="related" divided> {DEPLOYS.slice(0, 4).map(d => ( <Row key={d.id} justify="between" gap="group"> <Stack gap="hair"><Text>{d.project}</Text><Text role="caption" subtle>{d.message}</Text></Stack> <Button size="sm" emphasis="ghost" tone="neutral" onClick={() => show(d)}>Details</Button> </Row> ))} <Drawer open={open} title={picked.value.message} description={`${picked.value.project}, ${picked.value.id}`}> <Stack gap="related"> <Row justify="between"><Text subtle>Status</Text><Badge tone={TONE_OF[picked.value.status as Status]} dot>{picked.value.status}</Badge></Row> <Row justify="between"><Text subtle>Author</Text><Text>{picked.value.author}</Text></Row> <Row justify="between"><Text subtle>Branch</Text><Text>{picked.value.branch}</Text></Row> <Row justify="between"><Text subtle>Commit</Text><Text role="code">{picked.value.commit}</Text></Row> <Row justify="between"><Text subtle>Took</Text><Text>{picked.value.minutes} min</Text></Row> </Stack> </Drawer> </Stack> )}type Status = 'Live' | 'Building' | 'Failed' | 'Paused'const TONE_OF: Record<Status, 'positive' | 'info' | 'critical' | 'neutral'> = { Live: 'positive', Building: 'info', Failed: 'critical', Paused: 'neutral' }const DEPLOYS = [ { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' }, { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' }, { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' }, { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' }, { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' }, { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const min = 60_000Installation
npm install @weft-js/warpUsage
tsx
import { Drawer } from '@weft-js/warp'Placement
Use side to choose the edge. The default, auto, is the right side, and the bottom on phones.
Right
tsx
import { state } from '@weft-js/core'import { Button, Drawer, Text } from '@weft-js/warp'export function Right() { const open = state(false) return ( <> <Button emphasis="outline" tone="neutral" onClick={() => (open.value = true)}>Right</Button> <Drawer open={open} side="right" title="Deploy d-4821"> <Text subtle>For details of what you pressed.</Text> </Drawer> </> )}Left
tsx
import { state } from '@weft-js/core'import { Button, Drawer, Text } from '@weft-js/warp'export function Left() { const open = state(false) return ( <> <Button emphasis="outline" tone="neutral" onClick={() => (open.value = true)}>Left</Button> <Drawer open={open} side="left" title="Projects"> <Text subtle>For navigation.</Text> </Drawer> </> )}Top
tsx
import { state } from '@weft-js/core'import { Button, Drawer, Text } from '@weft-js/warp'export function Top() { const open = state(false) return ( <> <Button emphasis="outline" tone="neutral" onClick={() => (open.value = true)}>Top</Button> <Drawer open={open} side="top" title="Search"> <Text subtle>For a search or a short notice.</Text> </Drawer> </> )}Bottom
tsx
import { state } from '@weft-js/core'import { Button, Drawer, Text } from '@weft-js/warp'export function Bottom() { const open = state(false) return ( <> <Button emphasis="outline" tone="neutral" onClick={() => (open.value = true)}>Bottom</Button> <Drawer open={open} side="bottom" title="Share"> <Text subtle>For a quick task, and on phones.</Text> </Drawer> </> )}Filters
A drawer holds a longer set of controls beside the page, with footer for its buttons.
tsx
import { derived, state } from '@weft-js/core'import { Button, Checkbox, Drawer, Row, Stack, Switch, Text } from '@weft-js/warp'export function Filters() { const open = state(false) const failed = state(false) const regions = state(['eu-west']) const flip = (r: string) => (regions.value = regions.value.includes(r) ? regions.value.filter(x => x !== r) : [...regions.value, r]) const count = derived(() => regions.value.length + (failed.value ? 1 : 0)) const clear = () => { regions.value = []; failed.value = false } return ( <> <Button emphasis="outline" tone="neutral" icon="filter" onClick={() => (open.value = true)}>Filters ({count.value})</Button> <Drawer open={open} title="Filter deploys" footer={<Row gap="related" justify="between"><Button emphasis="ghost" tone="neutral" onClick={clear}>Clear</Button><Button data-close="">Show results</Button></Row>}> <Stack gap="section"> <Stack gap="related"> <Text role="label">Region</Text> {FILTER_REGIONS.map(r => <Checkbox key={r} checked={regions.value.includes(r)} onChange={() => flip(r)}>{r}</Checkbox>)} </Stack> <Switch checked={failed.value} onChange={() => (failed.value = !failed.value)}>Failed deploys only</Switch> </Stack> </Drawer> </> )}const FILTER_REGIONS = ['eu-west', 'us-east', 'ap-south']When to use
Playground
Side
import { Drawer } from '@weft-js/warp'<Drawer open={open} side="right" title="Deploy d-4821" description="storefront, on main"> ...</Drawer>Accessibility
- Esc
- Closes and returns focus to the opener
- Tab
- Stays inside the drawer
- It is a native
<dialog>, so the page behind is inert. - Focus goes to
autofocus, else the title, never the handle.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
open | State<boolean> | — | A state holding whether it is open. The drawer writes false when people close it. |
title | unknown | — | Names the drawer. |
description | unknown | — | A line under the title. |
footer | unknown | — | Stays at the bottom. Anything with data-close closes the drawer. |
side | "left" | "right" | "auto" | "top" | "bottom" | 'auto' | Which edge. auto is the right, or the bottom under 600px, decided when it opens. |
inline | boolean | false | Draws it open in the page's flow at its side's shape: no top layer, scrim or focus trap. For docs and previews; open still shows and hides it. |
children | unknown | — | What it contains. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="sheet"] [data-part="handle"]
handleheadertitledescriptionclosebodyfooter