Drawer
since v11.3.0Side-anchored modal panel for edit/detail flows beside a <DataTable>.
Wraps @radix-ui/react-dialog on the same primitive as {@link Dialog }, and
reuses Dialog's stacked-modal infrastructure (useModalDepth /
ModalDepthProvider / nestedModalZ) so nested-modal z-stacking stays
consistent across both surfaces. Radix owns the focus-trap, ESC + click-
outside dismissal, portaling, and ARIA wiring; we own the public API and the
edge-anchored, full-height visual surface.
Phase 1 renders an overlay + scrim at all widths. The responsive
push/shrink-table mode is deferred (Phase 1.5) and will arrive as an additive
optional prop — nothing in this API blocks it.
Install
Add the package and import the component.
pnpm add @hey-mike/tungstenpnpm add @hey-mike/tungstenimport { Drawer } from '@hey-mike/tungsten';import { Drawer } from '@hey-mike/tungsten';Preview
Same fixtures used by the visual-regression suite.
Usage
apps/docs/app/snapshots/drawer/page.tsx
'use client';
import { useState } from 'react';
import { Button, Drawer, Input } from '@hey-mike/tungsten';
export default function DrawerSnapshot() {
// Controlled open so the panel renders for the snapshot; the no-op handler
// keeps it pinned open (Radix would otherwise close on ESC / scrim click).
const [open] = useState(true);
return (
<main
data-testid="snapshot-root"
className="bg-page text-ink-1 min-h-screen p-8"
>
<h1 className="text-ink-2 mb-6 font-mono text-sm uppercase tracking-widest">
Drawer
</h1>
<Drawer
open={open}
onOpenChange={() => undefined}
side="right"
title="Edit user"
description="Update the team member's details."
>
<div className="flex flex-col gap-4">
<Input label="Name" defaultValue="Amara Reyes" />
<Input label="Email" defaultValue="amara@luoforge.com" />
<div className="mt-2 flex justify-end gap-3">
<Button variant="outline">Cancel</Button>
<Button variant="primary">Save</Button>
</div>
</div>
</Drawer>
</main>
);
}
'use client';
import { useState } from 'react';
import { Button, Drawer, Input } from '@hey-mike/tungsten';
export default function DrawerSnapshot() {
// Controlled open so the panel renders for the snapshot; the no-op handler
// keeps it pinned open (Radix would otherwise close on ESC / scrim click).
const [open] = useState(true);
return (
<main
data-testid="snapshot-root"
className="bg-page text-ink-1 min-h-screen p-8"
>
<h1 className="text-ink-2 mb-6 font-mono text-sm uppercase tracking-widest">
Drawer
</h1>
<Drawer
open={open}
onOpenChange={() => undefined}
side="right"
title="Edit user"
description="Update the team member's details."
>
<div className="flex flex-col gap-4">
<Input label="Name" defaultValue="Amara Reyes" />
<Input label="Email" defaultValue="amara@luoforge.com" />
<div className="mt-2 flex justify-end gap-3">
<Button variant="outline">Cancel</Button>
<Button variant="primary">Save</Button>
</div>
</div>
</Drawer>
</main>
);
}
Props
Surface specific to <Drawer />.
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | — |
| onOpenChange* | (open: boolean) => void | — | — |
| side | "right" | "left" | right | Edge the panel anchors to. @defaultValue 'right' |
| title | string | — | Visible heading; rendered as the dialog's accessible name. |
| description | string | — | Optional supporting text under the title. |
| aria-labelledby | string | — | When you render your own heading, pass its id here for the accessible name. |
| className | string | — | — |