Skip to content
LuoForge/Tungsten
Components/Drawer·Experimental

Drawer

since v11.3.0

Side-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/tungsten
pnpm add @hey-mike/tungsten
import { 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 />.

PropTypeDefaultDescription
open*boolean
onOpenChange*(open: boolean) => void
side"right" | "left"rightEdge the panel anchors to. @defaultValue 'right'
titlestringVisible heading; rendered as the dialog's accessible name.
descriptionstringOptional supporting text under the title.
aria-labelledbystringWhen you render your own heading, pass its id here for the accessible name.
classNamestring

Source