Malevich

Architecture decision record

ADR-0013 · Form subsystem

ADR 0013 — Form subsystem separation

Status

Accepted, 2026-05-18.

Source: v1.0 architectural quiz outcomes (docs-internal/architecture/v1-master-diff-merge.md §1.2, §2.1).

Context

The source spec for v1.0 treated form-context controls as variants of generic primitives — a "Form-button" was just <button type="submit" class="button">. The same approach treated Field-group as a thin composite around an Input.

Practical work on the 53-component v1.0 inventory exposed two problems with this collapse:

  1. Form-context buttons carry contracts the generic Button does not. A submit button can be in states like submitting (async pending), validation-error (form-level validation blocking), success-confirmed (post-submission). A generic Button has no need for any of these. Folding them into Button as variants either pollutes the generic API or leaves form authors hand-rolling markup and state.
  2. Form-divider has no generic equivalent. "Or sign in with" between auth options, or "Personal information" / "Billing information" between field groups — these are dividers with text embedded for form composition, not generic content separation. Folding them into Divider would force Divider to carry form-aware typography and spacing for a case Divider does not otherwise need.
  3. Composite forms need a coherent vocabulary. A real form is Field-group { Label, Input, HelperText, Error } × N + Form-button. Treating each member as ad-hoc means every form template re-derives the composition. Naming them as a family establishes the pattern.

The collapse felt clean in theory ("reuse the primitives") but failed the practical test: every real form ended up reaching for additional markup, state classes, or one-off helpers.

Decision

Form-context controls form a subsystem distinct from generic Elements. The subsystem lives under elements/form/ and contains:

Form-button is separate from Button

Form-button extends Button visually but adds form-context concerns:

Concern Button Form-button
Default type button submit (configurable)
States hover, active, disabled, focus + submitting, validation-error, success-confirmed
ARIA aria-pressed if toggle aria-busy during submit, integrates with form-level aria-invalid
Loading affordance optional, generic required during submitting, typically a Spinner replacement of icon
Variant defaults all 5 (primary/secondary/ghost/danger/success) typically primary for submit, ghost for cancel; same 5 available

Implementations may share CSS classes through token-level inheritance: form-button may consume --button-* tokens for visual consistency, overriding only the state-specific concerns. The component remains distinct because its API and state machine are distinct.

Form-divider is separate from Divider

Divider is a Foundation: a CSS-only horizontal or vertical rule. Form-divider is an Element: a horizontal divider with embedded text designed for form composition.

<!-- Divider (foundation) -->
<hr class="divider" />

<!-- Form-divider (element/form) -->
<div class="form-divider">
  <span>or sign in with</span>
</div>

Form-divider has typography (font-body-support or font-caption), spacing tuned for form groups, and optional alignment (text on left, center, right). None of these apply to generic Divider.

Field-group is the canonical composite

A Field-group renders:

[Label] [required/optional indicator]
[Input | Textarea | Select | RadioGroup | CheckboxGroup]
[HelperText OR Error]

It owns:

Author markup is small:

<div class="field-group" data-state="error">
  <label class="label" for="email">Email</label>
  <input class="input" type="email" id="email" aria-describedby="email-hint email-error">
  <p class="helper-text" id="email-hint">We'll never share your email</p>
  <p class="error" id="email-error">Enter a valid email address</p>
</div>

Field-group requires no runtime. State is driven by data-state attribute on the wrapper; CSS handles visibility.

What stays in generic Button

Generic Button retains all five variants (primary/secondary/ghost/ danger/success) and remains the right choice for:

Authors choosing between Button and Form-button apply a simple test: Is the button submitting or resetting a form? Yes → Form-button. No → Button.

Consequences

Positive:

Negative / tradeoffs:

Alternatives considered

References