Button

A trigger that initiates an action. Five variants — primary, secondary, ghost, critical, link — each with its own emphasis and use context.

Button — Accessibility

Target: WCAG 2.2 AAA. The recipe is conformant out of the box; adapters that consume the recipe inherit conformance unless they deviate from the canonical token references.

Conformance status

CriterionLevelStatus
1.4.3 Contrast (Minimum) — text vs backgroundAAPass — primary variant exceeds 7:1
1.4.6 Contrast (Enhanced)AAAPass — primary variant 7.4:1
1.4.11 Non-text Contrast — focus ringAAPass — 3:1 minimum against any tenant background
2.1.1 KeyboardAPass — Tab to focus, Space/Enter to activate
2.4.7 Focus VisibleAAPass — 2px ring, 2px offset
2.5.5 Target Size — 44x44 minimumAAAPass — 40px height + 2px ring + 2px offset = 44px
4.1.2 Name, Role, ValueAPass — native <button> element

Keyboard interactions

KeyBehaviour
TabMove focus to the button
Shift + TabMove focus to the previous focusable element
SpaceActivate the button
EnterActivate the button
Escape (in dialog context)Cancel the dialog without activating

Screen reader behaviour

The native <button> element is announced as "button" with the visible label. For icon-only buttons, set aria-label with the action description. The substrate's recipe never overrides the native role with role="button" on a non-button element; that pattern is an anti-pattern that breaks keyboard activation.

When a button enters a loading state (subsequent milestone), aria-busy="true" and aria-disabled="true" are set; the visible label updates to reflect the in-flight state.

Reduced motion

Buttons respect prefers-reduced-motion: reduce automatically — the CSS transition-duration value resolves to 0 when the media query matches. No code change required at the consumer level.

@media (prefers-reduced-motion: reduce) {
  .ps-btn { transition: none; }
}

Touch targets

All button variants meet the 44x44 minimum touch target. The component renders at 40px height; the focus ring offset adds 4px on each side, bringing the activatable area to 44px+. Dense-grid contexts that need smaller buttons should use the Ghost variant in a context that does not require touch — Ghost in a toolbar above a keyboard-focused work surface is acceptable; Ghost in a mobile-first form is not.

Colour independence

No button state is communicated by colour alone. Hover adds a darker background tone; pressed adds a darker still; disabled changes both background AND text colour AND removes the focus ring. Critical variants are marked by colour AND by their positioning (always paired with a confirmation step) AND by their label semantics (Delete account, Discard changes).

Anti-patterns

  • <div onclick> instead of <button>. Loses keyboard activation, screen-reader role, focus management. Always use <button>.
  • Disabled without explanation. The user has no way to know what would unlock the action. Surface the constraint near the trigger instead.
  • Critical variants with non-destructive labels. The visual weight is reserved for destructive actions. Don't use Critical for "Submit" or "Apply".
  • Focus ring removed. outline: none without an alternative breaks 2.4.7. The recipe never removes the ring.

Compliance audit endpoint

Future v0.0.3 work — the substrate will expose GET /api/audit/wcag?theme=<theme> returning a per-component WCAG conformance report. Today, this page is the canonical conformance statement.