Foundations

Interaction States

Every interactive component declares the same set of states, drawn with the same tokens, and operable the same way by keyboard and assistive tech. This is the contract — a component isn't done until each applicable state is handled.

Overview

Missing states — no loading button, no visible focus on a custom control, an invalid field with no message — are the most common real-world UX and accessibility defects. This page makes them a checklist, not an afterthought. Three principles:

  • Token-driven. Each state maps to a semantic token (--caat-color-state-*, --caat-focus, --caat-opacity-disabled) — never a per-component value.
  • Keyboard is not optional. Anything operable by mouse is operable by keyboard, with a visible focus indicator (AODA / WCAG 2.2 AA).
  • State is never colour alone. Error, selected, and active states carry a non-colour cue too (icon, text, weight, indicator).

The State Matrix

Nine states. Not every component has all nine — but the contract must say which apply and how each looks & behaves.

StateWhenTreatment (token)ARIA / keyboard
DefaultResting.Component base styles.
HoverPointer over an actionable element.Darken action (--caat-color-action-*-hover) or --caat-color-state-hover-bg. Hover lift uses one tier per component scale: --caat-lift-subtle (-1px, buttons) · --caat-lift-row (-2px, rows/tiles) · --caat-lift-card (-3px, cards) · --caat-lift-panel (-4px, hero panels).Pointer only — never the sole affordance.
Focus-visibleKeyboard focus.--caat-focus ring (3:1, light & dark).:focus-visible. Must be clearly visible.
Active / pressedMoment of activation.Darken to the action's -active shade (e.g. --caat-color-action-primary-active).:active; toggle buttons use aria-pressed.
DisabledNot currently available.Muted fill (--caat-color-state-disabled-bg) for filled controls, or opacity: var(--caat-opacity-disabled); not focusable.disabled or aria-disabled="true". Contrast-exempt.
Loading / busyAwaiting async result.Spinner; keep width to avoid layout shift.aria-busy="true"; disable re-submit.
Error / invalidValidation failed.--caat-color-state-invalid border + message + icon.aria-invalid="true", aria-describedby → message.
Selected / currentChosen / current location.--caat-color-state-selected-bg + non-colour cue.aria-current / aria-selected.
Read-onlyVisible, not editable (≠ disabled).Muted fill; text stays full-contrast.readonly — still focusable & copyable.

Buttons & actions

ContinueDefault
ContinueHover
ContinueFocus-visible
ContinueActive
ContinueDisabled
Saving…Loading

Hover/focus/active are shown statically here for comparison; in the real component they're triggered by :hover, :focus-visible, and :active.

Form fields

Enter a valid email, e.g. name@org.ca.

Selectable items

OverviewDefault
OverviewHover
OverviewSelected

Focus Management

A keyboard user must always know where they are and never get trapped. These rules are non-negotiable for AODA.

  • Use :focus-visible, not :focus. Show the ring for keyboard/AT users without flashing it on every mouse click.
    .my-control:focus-visible { outline: none; box-shadow: var(--caat-focus); }
  • Focus order follows DOM order. Never use positive tabindex. Composite widgets (tabs, menus, radio groups) use a single tab stop + arrow keys (roving tabindex).
  • Overlays trap & restore focus. Modal, off-canvas, and mega-menu move focus in on open, cycle Tab within, close on Esc, and return focus to the trigger on close. (Bootstrap's JS does this — custom overlays must too.)
  • Skip link & landmarks. Every page starts with a “Skip to main content” link (.skip-link) and uses landmark regions (header/nav/main/footer) so AT users can jump around.
  • Focus stays visible. Don't let sticky headers cover the focused element (WCAG 2.2 — 2.4.11 Focus Not Obscured); we set scroll-padding-top for this.

Target Size

WCAG 2.2 · 2.5.8 (AA) Pointer targets must be at least 24 × 24 CSS px, or have 24px of clear spacing around them.

  • Aim for 44 × 44 px for primary touch targets (buttons, nav items, form controls) — comfortable on mobile.
  • Small icon buttons (close, edit) must still hit 24px — pad the hit area even if the glyph is smaller.
  • Inline text links inside a paragraph are exempt, but stand-alone link buttons are not.

Keyboard Reference

Expected keys per interaction pattern (follows the WAI-ARIA Authoring Practices). Every interactive component's contract must state which applies.

PatternKeys
ButtonEnter / Space activate
LinkEnter activates
CheckboxSpace toggles
Radio groupTab enters group; move & select
Select / combobox move; type-ahead; Enter choose; Esc close
Menu / DropdownEnter/Space/ open; move; Esc close & restore focus
Tabs (or ) move; Home/End; activate on focus or Enter
AccordionEnter/Space toggle header; Tab between headers
Modal / DialogFocus trapped; Tab cycles within; Esc closes & restores focus
Tooltip / PopoverShows on focus/hover; Esc dismisses
Slider/ adjust; Home/End; PageUp/PageDown large steps

Don't Rely on Colour Alone

WCAG · 1.4.1 (A) Colour can't be the only way a state is conveyed — ~8% of men can't distinguish red/green, and colour is lost in greyscale or bright sun.

Error

Red border + an icon + a text message. Never a red outline by itself.

Selected / current

Background change + a checkmark, bold weight, or underline/indicator bar.

Links in text

Distinguishable from body text by more than colour — weight 600 and/or underline.

Component Checklist

Drop this into the Accessibility section of every interactive component's contract. A component isn't “done” until each box is ticked or marked N/A with a reason.

  • ☐ Lists which of the nine states apply, and shows each.
  • ☐ Hover, focus-visible, and active are visually distinct from default and from each other.
  • ☐ Focus indicator uses --caat-focus and is visible on light and dark backgrounds.
  • ☐ Fully operable by keyboard; documents its keys (see the reference above).
  • ☐ Disabled is non-focusable; read-only stays focusable and copyable.
  • ☐ Loading uses aria-busy and prevents duplicate submits.
  • ☐ Invalid sets aria-invalid + aria-describedby and shows an icon + message.
  • ☐ Selected/current uses aria-current/aria-selected and a non-colour cue.
  • ☐ Pointer targets are ≥ 24px (aim 44px); meets the colour-independence rule (1.4.1).
  • ☐ Overlays trap focus and restore it to the trigger on close.

Rationale & references: docs/design-system-recommendations.md §B2–B3.