Panel
Card-like container with composable header, title, description, content, row, and footer primitives.
Installation
dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.
The hosted registry is not public yet because r.b4r7.dev does not resolve. Use this source checkout or a local registry preview until the endpoint returns 200.
Diffgazer packages are not yet published to npm. Until the first release, pack @diffgazer/ui and @diffgazer/keys from the repository and install those tarballs.
UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.
Usage
Examples
Composed
Frames
Tones
Focused
Corner labels
API Reference
Panel
PanelHeader
PanelTitle
PanelDescription
PanelContent
PanelRow
PanelFooter
PanelLabel
Accessibility
Notes
Frames
Pick one frame via the `frame` prop: hairline (default soft border + marker bar), rail (inline-start rail drawn in --border-strong so it reads as a deliberate frame rather than a stray divider), viewfinder (four corner brackets), surface (--surface-1 background with a hairline perimeter and a 1px inner top lip in --surface-1-highlight — the fill steps lighter in dark and darker in light, and the lip is what keeps both directions reading as raised). Frame is purely visual chrome and applies independently of tone and density.
Component tokens
The perimeter paints `--panel-border-color`, whose default chain is `--panel-border` → `--panel-hairline` → a 60% `--border` mix. Set `--panel-border` on the panel or any ancestor (defaults resolve through var() fallbacks, so ancestor scoping reaches them) to lift the enclosure to a full-strength border while header, footer, and row hairlines stay on `--panel-hairline` — that split is what keeps the nesting ladder (panel enclosure → inner card → row hairline) readable. `--panel-border-color` itself is internal: `focused` repoints it at `--border-strong` on the hairline and surface frames (rail draws its own `--border-strong` edge, viewfinder has no border), and descendants inherit it, which is how a Panel.Label chip edge tracks the perimeter in every state without restating the chain. The other consumer tokens are `--panel-bg` (default `--background`), `--panel-fg` (default `--foreground`), `--panel-hairline`, and `--panel-tone` (the header marker bar, which `tone` overrides). Bracket geometry rides `--viewfinder-size` (12px), `--viewfinder-weight` (1px), `--viewfinder-color`, and `--viewfinder-offset` (-1px, seating the arm on the border line).
Tone
`tone` is a pure visual border-color tint (info, success, warning, error, accent). No icon slot, no announce, no role machinery. For status messaging with icons, live regions, dismissable, or role=alert, use Callout instead.
Density
`density="default"` uses 14/20 padding rhythm; `density="compact"` uses 10/14. Header, Content, and Footer read padding from the root's data-density attribute via panel/panel.css.
Header marker
`PanelHeader marker="bar"` (default) renders a 4px foreground bar to the left of Title/Description. Set `marker="none"` for rail/custom layouts where the bar would clash.
Accessibility
A statically discoverable Panel.Title makes the initial root a <section> and auto-wires aria-labelledby; Panel.Description similarly auto-wires aria-describedby. React cannot inspect content created inside an opaque child component during SSR. For that shape, assign stable ids to the generated title and description and pass those ids to root aria-labelledby/aria-describedby. An explicit ARIA name still makes the default root a <section>. With no discoverable title or explicit name, the root stays a plain <div> (no nameless landmark).
Eyebrow tags
There is no Title `meta` prop on Panel (unlike Dialog) because the header has a right-slot for actions. Compose eyebrow tags (e.g. "MAIN", "PROD") as plain siblings inside PanelHeader; they land in the right slot, vertically centered next to action buttons.
Corner labels
Use Panel.Label variant='border' for a tab chip seated on the panel border, variant='gap' for a border cutout label, or variant='readout' to seat the label on the panel's top rule between the two bracket arms (no box — the arms are the frame). The border chip recipe is fixed: 11px bold uppercase at 0.1em tracking in --muted-foreground, a --surface-2 fill, and a 1px border reading the panel's own --panel-border-color, so the chip edge matches the enclosure while resting and while focused. The inline-start inset is constant at 1rem: a bracket arm is the same 12px whether the panel is resting or focused, so one inset clears it in every state and the label never moves when focus arrives. The readout still sits just past the arm and repaints in --ring while the pane is focused, so label and corners read as one instrument. Every label publishes data-variant and data-state='focused'; consumers do not hand-roll those offsets.
Focused pane
`focused` marks the panel as the active pane in a multi-pane layout: corner brackets appear on any frame drawn in --ring, at the same 12px/1px geometry the viewfinder frame rests at (seated on the border line), and a hairline or surface perimeter firms to --border-strong with them. It emits data-state="focused" and shifts no size — no padding, bracket-length, or shadow change — so toggling it never reflows the layout. It is a visual affordance only: it does not move DOM focus and does not change roles, names, or ARIA. Drive it from whatever pane-focus state the app already owns, and keep a real focus-visible outline on the interactive elements inside.
Reticle grammar
The corner brackets are a signature, not decoration, and they mean one thing: this is the pane the keyboard drives. Four rules, stated as prohibitions because those are the ones that get broken. (1) A panel that cannot receive keyboard focus must not use frame="viewfinder" and must not pass `focused`. (2) A screen renders at most one panel with data-state="focused" at any time — in full-screen tests, assert that the selector [data-slot="panel"][data-state="focused"] matches exactly one element. (3) frame="viewfinder" without `focused` is reserved for surfaces where the reticle is the subject rather than the chrome, such as a marketing hero lens; on product screens the reticle always means focus. (4) Geometry never encodes state: resting and focused brackets are both 12px arms at 1px stroke centered on the border line, and focus changes color only (--foreground → --ring, while a hairline or surface perimeter firms to --border-strong). A bracket thickened or lengthened to mean "focused" is out of grammar.
Source
Install via CLI: pnpm exec dgadd add ui/panel.
Highlighted source loads after this disclosure opens. Browse the source repository.