Skip to main content

Overview

Matter Vault uses a system of shared components that enforce consistent design across the entire application. Every authenticated page, panel, modal, badge, and form field must use these components. If a component doesn’t support what you need, extend it — don’t bypass it.

Page Structure

Used on every authenticated page. Renders the H1 title and gold rule divider. Never build a page header inline.
import { PageHeader } from "@/components/shared/page-header";

<PageHeader
  title={<>All <span className="text-gold italic">Matters</span></>}
  subtitle="Active and archived matters for this firm."
  size="lg"
  action={<Button variant="default" size="sm">+ New Matter</Button>}
/>
PropTypeDefaultNotes
titleReactNoderequiredWrap emphasis word in <span className="text-gold italic">
subtitleReactNodePlain prose below the title
actionReactNodeRight-aligned button
size"lg" | "md""lg"lg = top-level pages, md = inner tabs
Eyebrow labels are not used. The breadcrumb provides page context.

PanelHeader

Used on every panel, table, or list section. Companion to PanelFooter.
import { PanelHeader } from "@/components/ui/panel-header";

<PanelHeader
  title="Documents"
  count={131}
  unit="file"
  actions={<Button variant="outline" size="sm">Export</Button>}
/>
PropTypeNotes
titleReactNodePanel heading
countnumber | stringShown after title in muted mono
unitstringAuto-pluralized
actionsReactNodeRight-aligned button cluster
density"default" | "compact"py-4 vs py-3

PanelFooter

Used on every panel with pagination or item counts.
import { PanelFooter } from "@/components/ui/panel-footer";

<PanelFooter
  showing={25}
  total={131}
  unit="item"
/>

Forms

Field

Wraps every form label + input pair. Handles label, required indicator, hint text, and error messages.
import { Field, FIELD_INPUT_CLASS } from "@/components/ui/field";

<Field label="First Name" htmlFor="first-name" required>
  <input
    id="first-name"
    autoFocus
    className={FIELD_INPUT_CLASS}
  />
</Field>
PropTypeNotes
labelstringRequired
htmlForstringMust match input id
requiredbooleanShows red asterisk
hintReactNodeHelper text below input
errorReactNodeError message in red

Input classes

Always import — never redeclare locally:
import {
  FIELD_INPUT_CLASS,           // standard height
  FIELD_INPUT_CLASS_COMPACT,   // h-9 compact variant
  FIELD_LABEL_CLASS,           // raw label class (prefer <Field>)
} from "@/components/ui/field";

FieldRow

Used for display-mode label + value pairs in detail cards and sidebars.
import { FieldRow } from "@/components/dashboard/field-row";

<FieldRow label="Firm" value={me.firm.name} />
<FieldRow label="Matter Number" value="MV-2026-011" mono />
<FieldRow label="Notes" value={matter.notes} layout="stacked" />

Modals

All modals use this wrapper. Never use DialogContent directly.
import { Modal } from "@/components/ui/modal";

<Modal
  open={open}
  onOpenChange={setOpen}
  title="Add Contact"
  size="md"
  disableOutsideClick={true}
  footer={
    <>
      <button onClick={() => setOpen(false)} className={cancelClass}>
        Cancel
      </button>
      <Button
        variant="default"
        disabled={!isValid || isPending}
      >
        Save contact
      </Button>
    </>
  }
>
  <Field label="First Name" htmlFor="first-name" required>
    <input id="first-name" autoFocus className={FIELD_INPUT_CLASS} />
  </Field>
</Modal>
SizeMax widthUse for
smmax-w-smConfirmation modals
mdmax-w-lgStandard form modals (default)
lgmax-w-2xlLarge form modals
xlmax-w-4xlWizards and multi-step flows
Enforced behaviors:
  • ESC always closes
  • Click outside: blocked on form modals (disableOutsideClick={true}), allowed on read-only modals
  • First input gets autoFocus
  • Submit always has disabled={!isValid || isPending}
  • No eyebrow text inside modals
  • Always vertically centered

ConfirmModal

Every destructive action must use this. Never show a delete button without confirmation.
import { ConfirmModal } from "@/components/ui/confirm-modal";

<ConfirmModal
  open={confirmOpen}
  onOpenChange={setConfirmOpen}
  title="Delete document?"
  description="This will permanently remove the document from this matter."
  confirmLabel="Delete document"
  onConfirm={handleDelete}
  isPending={isDeleting}
/>
The user must check “I understand this action cannot be undone” before the delete button enables.

Badges

Badge

All colored chips, status labels, and role badges use this component.
import { Badge } from "@/components/ui/badge";

<Badge tone="teal">Active</Badge>
<Badge tone="red">Legal Hold</Badge>
<Badge tone="gold">In Review</Badge>
<Badge tone="muted">Closed</Badge>
<Badge tone="blue">Medical Records</Badge>
<Badge tone="amber">Low Balance</Badge>
ToneUse for
tealActive, success, confirmed, clean
redError, critical, hold, adversarial
goldWarning, pending, review, draft
amberCaution, low balance
mutedClosed, inactive, terminal stages
blueDocument categories, informational
neutralCustom labels with no semantic color

Empty States

EmptyState

All empty panel states use this. Never build inline empty states.
import { EmptyState } from "@/components/ui/empty-state";

<EmptyState preset="no-documents" size="md" />
<EmptyState preset="no-contacts" size="sm" />
<EmptyState preset="no-activity" size="row" />
The row size renders a single italic line for in-panel one-liners.

Banned Patterns

Never do these. If you see them in existing code, fix them.
PatternUse instead
Local INPUT_CLASS / LABEL_CLASS constsImport from components/ui/field.tsx
Inline page headers<PageHeader>
Eyebrow text inside modalsRemove — never use
text-[9px] or smallerMinimum is text-[10px]
Hardcoded hex colorsToken classes only (text-gold, bg-teal)
bg-raised on inputsbg-surface only
variant="default" on non-primary actionsvariant="outline" or variant="ghost"
Raw <button> for icon affordances<Button variant="ghost" size="icon">
Delete without confirmation<ConfirmModal>
DialogContent directly<Modal> wrapper
font-semibold on serif headingsRemove — no-op on Cormorant Garamond
Local badge color maps<Badge tone="...">
text-4xl on inner tab pagestext-2xl via <PageHeader size="md">