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.
| State | When | Treatment (token) | ARIA / keyboard |
|---|---|---|---|
| Default | Resting. | Component base styles. | — |
| Hover | Pointer 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-visible | Keyboard focus. | --caat-focus ring (3:1, light & dark). | :focus-visible. Must be clearly visible. |
| Active / pressed | Moment of activation. | Darken to the action's -active shade (e.g. --caat-color-action-primary-active). | :active; toggle buttons use aria-pressed. |
| Disabled | Not 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 / busy | Awaiting async result. | Spinner; keep width to avoid layout shift. | aria-busy="true"; disable re-submit. |
| Error / invalid | Validation failed. | --caat-color-state-invalid border + message + icon. | aria-invalid="true", aria-describedby → message. |
| Selected / current | Chosen / current location. | --caat-color-state-selected-bg + non-colour cue. | aria-current / aria-selected. |
| Read-only | Visible, not editable (≠ disabled). | Muted fill; text stays full-contrast. | readonly — still focusable & copyable. |
Buttons & actions
Hover/focus/active are shown statically here for comparison; in the real component they're triggered by :hover, :focus-visible, and :active.
Form fields
Selectable items
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 (rovingtabindex). - Overlays trap & restore focus. Modal, off-canvas, and mega-menu move focus in on open, cycle
Tabwithin, close onEsc, 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-topfor 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.
| Pattern | Keys |
|---|---|
| Button | Enter / Space activate |
| Link | Enter activates |
| Checkbox | Space toggles |
| Radio group | Tab enters group; ↑↓←→ move & select |
| Select / combobox | ↑↓ move; type-ahead; Enter choose; Esc close |
| Menu / Dropdown | Enter/Space/↓ open; ↑↓ move; Esc close & restore focus |
| Tabs | ←→ (or ↑↓) move; Home/End; activate on focus or Enter |
| Accordion | Enter/Space toggle header; Tab between headers |
| Modal / Dialog | Focus trapped; Tab cycles within; Esc closes & restores focus |
| Tooltip / Popover | Shows 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-focusand 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-busyand prevents duplicate submits. - ☐ Invalid sets
aria-invalid+aria-describedbyand shows an icon + message. - ☐ Selected/current uses
aria-current/aria-selectedand 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.