Skip to content
weftGitHub

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_000

Installation

npm install @weft-js/warp

Usage

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

Use it for

  • Details of something picked from a list
  • A longer form or a set of filters

Reach for something else

  • Modal, when it is a decision, not a task
  • Popover, when a few controls near the button will do

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

PropTypeDefaultDescription
openState<boolean>—A state holding whether it is open. The drawer writes false when people close it.
titleunknown—Names the drawer.
descriptionunknown—A line under the title.
footerunknown—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.
inlinebooleanfalseDraws 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.
childrenunknown—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

RelatedModalPopover