Brik Design System
Components

Sheet

Sliding panel anchored to a screen edge. Read / edit modes, tabs, secondary action, floating variant.

Sheet is the canonical detail-view panel — slides in from a screen edge for record views, edit workflows, and secondary content that shouldn't replace the current page. Mode-driven footer (Read / Edit) auto-renders the right action set; pass tabs for provenance/history surfaces; pair with SheetSection and the Sheet typography primitives for a locked-down body composition.

Use it for

  • Record detail views (company profile, project brief, contact card)
  • Edit workflows that compose multi-section forms
  • Drilldowns from table rows or dashboard tiles
  • Anywhere a Modal would feel too interruptive but the content needs more space than a Popover

Import

import { Sheet, SheetSection } from '@brikdesigns/bds';

Modes

Sheet has two modes that map to the most common UX pattern across the product suite:

  • read — view-only display. Footer (when shown) renders [Close] [Edit].
  • edit — active form state. Footer renders [Cancel] [Save].

The canonical pattern: open in read, click Edit to switch to edit, Save / Cancel returns to read. Mirrors the renew-pms convention.

import { useState } from 'react';

function CompanySheet() {
  const [open, setOpen] = useState(false);
  const [mode, setMode] = useState<'read' | 'edit'>('read');

  return (
    <Sheet
      isOpen={open}
      onClose={() => setOpen(false)}
      subtitle="Company"
      title="Brik Designs"
      description="Active · Updated 2 days ago"
      mode={mode}
      onEdit={() => setMode('edit')}
      onSave={async () => { await save(); setMode('read'); }}
      onCancel={() => setMode('read')}
    >
      {mode === 'read' ? <ReadOnlyFields /> : <EditFormFields />}
    </Sheet>
  );
}

Don't put Edit as a button in the body. Use mode="read" + onEdit so it surfaces in the auto-footer where users expect primary actions.

The header composes four optional pieces:

  • subtitle — uppercase eyebrow above the title (entity type, parent record). Renders in text-muted.
  • title — main heading, rendered as <h2>.
  • description — long-form secondary text below the title (record state, timestamps).
  • onBack — back button for nested sheet navigation. Pair with your sheet stack controller.
<Sheet
  subtitle="Strategic Brief"
  title="Birdwell & Mutlak"
  description="Draft · Last edited 3 days ago"
  onBack={popSheet}
>
  ...
</Sheet>

Sides

  • right (default) — slides from right edge
  • left — for navigation or sidebar panels
  • bottom — full-width drawer, mobile-friendly
<Sheet side="bottom">...</Sheet>

Variants

  • default — full-height overlay with backdrop (forms, edit workflows)
  • floating — rounded floating panel with elevation, no backdrop (read-only detail views, inline drill-ins from a table row)
<Sheet variant="floating" mode="read" onEdit={...}>...</Sheet>

Tabs

Tabs render below the header. When tabs is supplied, children is ignored — each tab supplies its own content. Use for separating Details from Sources / History / Activity.

<Sheet
  title="Birdwell & Mutlak"
  tabs={[
    { id: 'details', label: 'Details', content: <DetailsTab /> },
    { id: 'sources', label: 'Sources', content: <SourcesTab /> },
    { id: 'history', label: 'History', content: <HistoryTab /> },
  ]}
  activeTab={tab}
  onTabChange={setTab}
/>

Keep the first tab's id stable (e.g. 'details') — it's the default active tab when activeTab is uncontrolled.

Secondary action

For ancillary actions next to the primary Edit / Save (e.g. Refresh Brief, Run Extraction), pass secondaryAction instead of composing a custom footer.

<Sheet
  mode="read"
  onEdit={() => setMode('edit')}
  secondaryAction={{
    label: 'Refresh brief',
    onClick: handleRefresh,
    iconBefore: <RefreshIcon />,
  }}
>
  ...
</Sheet>

Behavior:

  • Read mode — renders [Secondary] . . . [Close] [Edit]
  • Edit mode — secondary action is suppressed (Save's commit surface stays unambiguous)
  • Custom footer — wins over secondaryAction

Body composition

Inside a Sheet body, reach for the sheet primitive set first. Don't invent ad-hoc markup.

PrimitiveUse for
SheetSectionNamed section wrapper. One per logical grouping.
FieldLabel + value pair.
FieldGrid2/3/4-column grid of Fields or Cards.
TagGroupTag clusters inside a Field.
BulletListShort-item lists.
Card / CardListEntity rows.
TableTabular data.
AccordionCollapsible groups when a section has many rows.
EmptyStateWhole-section "no data" treatment.

Don't reach for raw <h3> / <table> / <ul> inside a Sheet body — each has a primitive that locks the type scale and spacing. Use the Sheet typography primitives for inline labels and values.

When to use which treatment

ScenarioVariantFooter
Read-only metadata, no CTAfloatingnone
Read detail with edit capabilitydefault, mode="read" + onEdit[Close] [Edit] (auto)
Active form / edit workflowdefault, mode="edit" + onSave[Cancel] [Save] (auto)
Read + ancillary actionmode="read" + onEdit + secondaryAction[Refresh] . . . [Close] [Edit]
Record with provenance / historyany mode + tabsmode-driven
Non-standard action setanycustom footer

When not to use

  • Don't use Sheet for short confirmations. Use Modal preset="confirm".
  • Don't use Sheet without a clear reason to leave the page. If the content fits inline, render it on the page directly.
  • Don't nest Sheets visually. Use onBack push-stack navigation — Sheets stack semantically, not visually.

Accessibility

  • Renders role="dialog" with aria-labelledby linking the title.
  • Focus trap inside the sheet; focus returns to the trigger on close.
  • Escape closes (default closeOnEscape={true}); backdrop click closes (default closeOnBackdrop={true} — no effect on floating variant since there's no backdrop).
  • Auto-footer Save button announces aria-busy while saveLoading={true}.

API

PropTypeDefault
isOpenboolean (required)
onClose() => void (required)
childrenReactNode
side'right' | 'left' | 'bottom''right'
titleReactNode
subtitleReactNode
descriptionReactNode
widthstring'400px'
variant'default' | 'floating''default'
closeOnBackdropbooleantrue
closeOnEscapebooleantrue
showCloseButtonbooleantrue
onBack() => void
mode'read' | 'edit'
onEdit / onSave / onCancel() => void
editLabel / saveLabel / cancelLabel / closeLabelstringlabel defaults
saveDisabled / saveLoadingbooleanfalse
footerReactNode (overrides auto-footer)
secondaryActionSheetSecondaryAction
tabsSheetTab[]
activeTabstring (controlled)first tab id
onTabChange(tabId: string) => void

On this page