EditableSection

A labeled section of related fields the user can view and edit — settings, profile dossiers, account info, contact records. Two variants via discriminated-union props: card-edit (admin surfaces, per-section atomic save with an Edit affordance) and form-save (consumer surfaces, form-wide commit at the bottom of the page). The canonical drift-paired-as-variants compound — both save models are valid for their surface tier.

Spec · from metadata

When to use

  • Account / settings pages broken into labeled field groups
  • Profile or member dossiers with sections like Contact, Demographics, Insurance
  • Any 'view, then edit' record surface where fields are grouped under headings
  • card-edit for admin products (per-section Edit + Save); form-save for consumer products (one Save at the bottom)

When not to use

  • A single field with inline edit — use an Input with its own save affordance
  • A read-only detail view with no editing — use SectionedDetailView or plain Card
  • A multi-step wizard — use Stepper + form fields
  • The page-level commit bar for form-save — that's a separate FormSaveBar at the bottom of the whole form, not part of EditableSection

Anti-patterns

Avoid// consumer account page
<EditableSection variant="card-edit" title="Basics" isEditing={e1} onSave={...}>...</EditableSection>
<EditableSection variant="card-edit" title="Address" isEditing={e2} onSave={...}>...</EditableSection>
// every section has its own Edit/Save — heavy for a consumer surface
Prefer// consumer account page
<form onSubmit={save}>
  <EditableSection variant="form-save" title="Basics">...</EditableSection>
  <EditableSection variant="form-save" title="Address">...</EditableSection>
  <div className="flex justify-end pt-4">
    <Button type="submit">Save changes</Button>
  </div>
</form>

Consumer surfaces use form-save: one commit at the bottom, lighter touch. Per-section card-edit is an admin pattern where users review and edit atomic sections. Matching the variant to the surface tier is the whole point of shipping both.

Avoid<EditableSection variant="form-save" title="Contact" onSave={save} isEditing={editing}>
  ...
</EditableSection>
Prefer<EditableSection variant="form-save" title="Contact">
  ...
</EditableSection>
// commit handled by the parent form's FormSaveBar

form-save has no editing state or per-section save — TypeScript refuses onSave/isEditing on this variant. The discriminated union enforces the right prop set per variant.

Accessibility

  • Min touch target48px
  • Screen readercard-edit: the Edit button is icon-only on small screens with an sr-only 'Edit' label, label+icon on sm+. form-save: renders a real <section> with a heading at the configured level (default h3) — never skip heading levels relative to the page's h1.
  • ContrastInherits Card / Button / Separator token contrast (WCAG AA). Edit button is ghost variant; Save is primary; Cancel is outline.

Token bindings

TokenCategoryUsage
cardcolorCard surface (from the Card primitive)
card-foregroundcolorSection title and body text
muted-foregroundcolorSection description text
bordercolorSeparator between the header and the section content
text-lg / font-semibold (form-save heading)typographySection heading scale
4 (16px) / 2 (8px)spacingSection content gaps / footer button gap
Recent changes
  • new

    Compound components join the catalog

    2026-07-09

[!NOTE] Compound component — an opinionated composition of Helix primitives. Composes: Card, Button, Separator.

Import

import { EditableSection } from "@timelycare/helix-ui"

Usage

// card-edit (admin) — controlled editing state
const [editing, setEditing] = useState(false)

<EditableSection
  variant="card-edit"
  title="Contact information"
  description="How we reach you about your visits."
  isEditing={editing}
  onEdit={() => setEditing(true)}
  onSave={() => { save(); setEditing(false) }}
  onCancel={() => setEditing(false)}
>
  {editing
    ? <Input value={email} onChange={...} />
    : <p className="text-sm">{email}</p>}
</EditableSection>

// form-save (consumer) — parent form commits everything
<form onSubmit={handleSubmit}>
  <EditableSection variant="form-save" title="Basics">
    <Input ... />
  </EditableSection>
  <EditableSection variant="form-save" title="Contact phone">
    <Input ... />
  </EditableSection>
  <div className="flex justify-end pt-4">
    <Button type="submit">Save changes</Button>
  </div>
</form>