Form-button
Submit / reset / cancel button with form-aware states. Per ADR-0013 —
distinct from generic Button because submit buttons carry states
(submitting, success, error) that generic Button does not.
When to use
- Inside a
<form>, as the primary submit action. - Reset / cancel buttons paired with a submit.
- Auth screens: "Sign in", "Create account".
For any button that does not submit or reset a form, use generic
Button (elements/button/). The decision test: is the button
submitting or resetting a form? Yes → Form-button. No → Button.
States
State is driven by data-state on the button. Transitioning states
is the consumer's responsibility (form submit handler sets it).
Per the answered design question recorded with this component:
| State | Visual | Set when |
|---|---|---|
| Default | Variant color | Initial render |
submitting |
Cursor: progress, indicator slot shows | Async submit in flight; pair with aria-busy="true" |
success |
Background: success color, check icon | Server returned OK; auto-revert after ~1.5s |
error |
Background: danger color, alert icon | Server returned error; revert on retry |
The visual states override the variant color so the form button clearly broadcasts outcome regardless of original styling.
Variants
| Variant | Class |
|---|---|
| Primary | .form-button.-primary |
| Secondary | .form-button.-secondary |
| Ghost | .form-button.-ghost |
| Danger | .form-button.-danger |
| Success | .form-button.-success |
Sizes: .-s and .-l (default = medium).
Anatomy
<!-- Default -->
<button type="submit" class="form-button -primary">Save</button>
<!-- Submitting -->
<button type="submit" class="form-button -primary"
data-state="submitting" aria-busy="true">
Saving
<span class="form-button__indicator" aria-hidden="true">
<span class="spinner -s -inverse" role="presentation"></span>
</span>
</button>
<!-- Success -->
<button type="submit" class="form-button -primary" data-state="success">
Saved
<span class="form-button__indicator" aria-hidden="true">✓</span>
</button>
<!-- Error -->
<button type="submit" class="form-button -primary" data-state="error">
Try again
<span class="form-button__indicator" aria-hidden="true">⚠</span>
</button>
Authors render all four state contents conditionally (or swap inner content on transition). A common pattern is a single inner indicator slot that the application updates on each state change.
Tokens used
--color-accent,--color-accent-strong— primary variant--color-ink-strong,--color-ink-inverse--color-border-strong— secondary border--color-surface-canvas— ghost hover--color-danger— danger variant + error state--color-success— success variant + success state--space-inset-element-*,--space-gap-elements-s--size-control-*,--radius-button--border-width-hairline/--border-width-focus--font-action-{s,m,l}-*— typography--motion-fast/--motion-easing-default
Accessibility
<button type="submit">carries the form-submission contract.aria-busy="true"duringdata-state="submitting"announces the busy state to assistive tech. Withoutaria-busy, screen readers do not know the submission is in flight.- The state indicator is
aria-hidden="true"— meaning is carried by the button's text label andaria-busy. - For success / error feedback that should be announced, pair with a
form-level
aria-liveregion that names the outcome ("Saved", "Couldn't save — try again"). The button visual is reinforcement, not the only signal.
Edge cases
- Hover during submitting: the
:hoverrule excludes[data-state]so the button doesn't re-tint to hover during submission. - Multiple submits: disable the button (set
disabledattribute) when entering submitting state to prevent double-submits. - Reset buttons: use the same component with
type="reset". The state transitions still apply if reset triggers any async work; for pure native reset, no state changes are needed.
Do
- Use
<button type="submit">for the primary form action. - Pair
data-state="submitting"witharia-busy="true"anddisabled(oraria-disabled="true"). - Announce success / error via a form-level
aria-liveregion.
Don't
- Don't use Form-button outside a
<form>— use Button instead. - Don't change the button text mid-submit if it confuses (e.g. don't swap "Save" for "Loading…" in the SAME slot if your design also shows a spinner; pick one signal or both consistently).
- Don't transition through
success/errorif the form-levelaria-liveregion already announces the outcome — pick one surface to avoid duplicate announcements.