# Cowork UI Kit — UX Patterns (Kit Layer)

> **Scope:** Brand-agnostic UX patterns the kit's token system + components should encode. Brand voice is a separate layer (see § "Brand voice as a token layer (future)") and out of scope for this document.
>
> **Audience:** This kit's primary consumer is an AI agent invoking `/kry-ui`. Patterns are written as decision rules + token recipes the agent can apply mechanically, not philosophy.
>
> **Date:** 2026-05-20 · v0.5.9 baseline
> **Companion docs:** 📄 [INTERACTION-RULES.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/INTERACTION-RULES.md) (mechanics) · 📄 [A11Y-RULES.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/A11Y-RULES.md) (POUR enforcement) · 📄 [FINAL-STATE.md](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/FINAL-STATE.md) (state-of-kit)

---

## How to read this document

Each pattern entry follows a strict structure:

```
### PX.Y — Pattern name
Status:     ✅ Implemented | ⚠️ Partial | ❌ Missing | 🔮 Future
What:       1-sentence definition
Why kit:    specific role in a KIT (not "good UX is important")
Authority:  primary reference + what it says
Recipe:     copy-pasteable HTML/CSS using existing tokens, or "needs: token X"
Anti:       what NOT to do, with reason
In kit:     where it lives now / where to add
```

Reject vague principles. Reject motivational filler.

---

## Section 1 — Motion patterns

### P1.1 — Tiered duration scale (4-step minimum)
**Status:** ✅ Implemented
**What:** A token scale of motion durations from "instant feedback" to "page transition", not arbitrary ms values per component.
**Why kit:** Without a scale, AI agents will copy arbitrary `250ms` / `350ms` values into components → motion feels inconsistent across pages. A scale forces a decision tree: "is this hover (fast) or a modal (slow)?"
**Authority:** 🌐 [Material 3 Easing & Duration Tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs) defines short1–short4 (50/100/150/200ms), medium1–4 (250–400ms), long1–4 (450–600ms), extra-long (700–1000ms). 🌐 [Carbon Motion](https://carbondesignsystem.com/elements/motion/overview/) defines productive (faster, under-200ms) vs expressive (slower, 400ms+) two-tier system.
**Recipe:** Use existing kit tokens — `--duration-fast` (120ms), `--duration-normal` (200ms), `--duration-slow` (320ms), `--duration-slower` (480ms). Map: hover/tap → fast · dropdown/toggle → normal · modal/drawer → slow · page transition → slower.
**Anti:** ❌ `transition: 250ms ease` literal · ❌ Single global `--transition` token (no scale) · ❌ Duration > 500ms for non-page-level UI (feels laggy per 🌐 [web.dev INP](https://web.dev/articles/inp) — good INP < 200ms).
**In kit:** 📄 [tokens/foundation.css](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/tokens/foundation.css) lines 242-246. ✅ Already enforced by INTERACTION-RULES.md § 1.

---

### P1.2 — Asymmetric easing (separate enter/exit, decelerate-emphasis on enter)
**Status:** ⚠️ Partial — kit has 6 easing curves but no enter/exit token pair convention.
**What:** Elements entering a scene should decelerate (ease-out emphasis); elements leaving should accelerate (ease-in emphasis). Symmetric `ease-in-out` feels mechanical for entrance/exit choreography.
**Why kit:** AI agents default to one easing for everything → modals close at same curve they open → loss of "the system is paying attention" feeling. Two-curve convention encodes the asymmetry mechanically.
**Authority:** Material 3 ships **Emphasized Decelerate** `cubic-bezier(0.05, 0.7, 0.1, 1)` for enter and **Emphasized Accelerate** `cubic-bezier(0.3, 0, 0.8, 0.15)` for exit (🌐 [Material tokens JSON](https://github.com/material-foundation/material-tokens/blob/json/json/motion.json)). 🌐 [Apple HIG Motion](https://developer.apple.com/design/human-interface-guidelines/motion) emphasizes "quick, precise animations" with realistic deceleration as objects come to rest.
**Recipe:** Needs new tokens — kit currently exposes `--ease-out`, `--ease-out-soft`, `--ease-in-out` but not paired enter/exit. Add to foundation.css:
```css
--ease-enter: cubic-bezier(0.05, 0.7, 0.1, 1);  /* Material emphasized decelerate */
--ease-exit:  cubic-bezier(0.3, 0, 0.8, 0.15);  /* Material emphasized accelerate */
```
Modal open uses `--ease-enter`, close uses `--ease-exit` + `--duration-normal` (close faster than open per INTERACTION-RULES § 8).
**Anti:** ❌ Same easing both directions · ❌ Bounce/spring on close (close should feel decisive, not playful) · ❌ Linear easing on UI feedback (per INTERACTION-RULES § 2).
**In kit:** Currently `--ease-out` `cubic-bezier(0.32, 0.72, 0, 1)` (iOS-flavored) is used as default. Enter/exit split is a NEW token addition.

---

### P1.3 — Choreographed stagger (sibling delays for list reveal)
**Status:** ❌ Missing — no stagger primitive in kit.
**What:** When N siblings appear simultaneously (list of cards, menu items), stagger their motion by 20–40ms increments so the eye reads them as a sequence, not a wall.
**Why kit:** Lists rendered with simultaneous fade-in feel like a "flash" (high cognitive load — Miller's Law, 🌐 [Laws of UX Miller](https://lawsofux.com/) caps working memory ~7 items). Stagger lets the eye chunk arrivals.
**Authority:** Material 3's "choreography" guidance recommends 20–50ms inter-sibling delay. Carbon's [motion choreography](https://carbondesignsystem.com/elements/motion/choreography/) defines a "leading" element that sets motion direction.
**Recipe:** Needs new token + utility class:
```css
--stagger-step: 30ms;
.stagger-children > * { animation-delay: calc(var(--stagger-step) * var(--i, 0)); }
/* Markup: <li style="--i:0">…</li><li style="--i:1">…</li> */
```
Or React: `transitionDelay: index * 30 + 'ms'`. Cap at 6 staggered children — beyond that the tail feels slow (Doherty 400ms ceiling).
**Anti:** ❌ Staggering destructive lists (delete confirm — user wants instant) · ❌ Stagger > 50ms per step (tail lags) · ❌ Staggering items behind-the-fold (delays nothing the user sees).
**In kit:** No reference implementation. Add to molecules/atoms patterns.

---

### P1.4 — Reduced-motion respect (non-negotiable)
**Status:** ✅ Implemented
**What:** A global `@media (prefers-reduced-motion: reduce)` override that shrinks animation-duration to ~0ms for users with vestibular conditions.
**Why kit:** WCAG 2.3.3 (Animation from Interactions, AAA) + ethical baseline. Without a kit-level override, every component author must remember this — and they will not.
**Authority:** 🌐 [WCAG 2.1 Quick Ref](https://www.w3.org/WAI/WCAG21/quickref/) § 2.3.3 · Apple HIG Motion explicitly: "minimize or eliminate animations" when Reduce Motion is on.
**Recipe:** Kit ships this in INTERACTION-RULES § "Reduced motion override". Confirm placement in `foundation.css` (currently in rules doc only — should also be IN the CSS).
**Anti:** ❌ Animation that conveys information without alternative (vestibular users miss it) · ❌ Parallax / 3D scroll without opt-out toggle · ❌ Auto-play motion > 5s without pause control.
**In kit:** 📄 INTERACTION-RULES.md § "Reduced motion override". **Gap:** Verify the block is also IN foundation.css, not only the rules doc.

---

### P1.5 — Spring physics for delight moments (not for routine UI)
**Status:** ✅ Implemented (`--ease-spring`, `--ease-bounce`)
**What:** Spring/bounce easing reserved for celebratory moments (like-button pop, success check, achievement). Routine UI (modal/drawer/dropdown) uses decelerate-only easing.
**Why kit:** Material 3 Expressive (2026) is pushing physics-based motion as default, but indiscriminate spring on every dropdown feels childish on serious/SaaS contexts. Kit must encode the **distinction** so AI agents pick the right one.
**Authority:** 🌐 [Material 3 Expressive blog](https://m3.material.io/blog/m3-expressive-motion-theming) — "mimic spring physics, easing in and out with elasticity" — but reserved for **expressive moments**. Carbon explicitly splits **productive motion** (subtle, fast) from **expressive motion** (vibrant, occasional).
**Recipe:** Kit tokens `--ease-spring` `cubic-bezier(0.5, 1.5, 0.5, 1)` for delight; `--ease-bounce` `cubic-bezier(0.68, -0.55, 0.265, 1.55)` for celebration only. Default for routine UI = `--ease-out`.
**Anti:** ❌ Spring on dropdown open (over-animated) · ❌ Bounce on modal (loses authority for destructive confirm) · ❌ Spring on data table row insert (cognitive distraction during work).
**In kit:** 📄 INTERACTION-RULES.md § 2 documents this split.

---

## Section 2 — Interaction patterns

### P2.1 — Immediate visual feedback (< 100ms ACK rule)
**Status:** ⚠️ Partial — button :active state exists but no kit-level "ACK pattern" doc.
**What:** Every interactive element must produce a visual change within 100ms of user input, even if the actual operation takes seconds. The ACK is independent from the operation.
**Why kit:** 🌐 [web.dev INP article](https://web.dev/articles/inp) — good INP ≤ 200ms at p75. Hitting that requires the kit to encourage "paint something NOW, do work after" patterns. Without this rule, AI agents write `await fetch() → setState()` which delays the ACK by RTT.
**Authority:** 🌐 [Doherty Threshold](https://lawsofux.com/doherty-threshold/) — 400ms is the productivity ceiling; ≤ 100ms feels instant. INP measures input → next paint, not input → operation complete.
**Recipe:** Two-state interactions:
```js
// ✅ Good — paint ACK in same frame, defer work
button.onclick = () => {
  button.classList.add('is-pressed');         // synchronous, < 16ms paint
  requestAnimationFrame(async () => {
    await doExpensiveWork();
    button.classList.remove('is-pressed');
  });
};
```
Pair with `--duration-fast` (120ms) for the :active style transition.
**Anti:** ❌ `await fetch()` before any UI change · ❌ Spinner replacing button label without holding minimum 200ms (anti-flash — INTERACTION-RULES § 7) · ❌ Form submit with no visible state on button.
**In kit:** Mention in INTERACTION-RULES § 5 active state, but no dedicated "feedback latency" doc. **Gap:** Add to AI-agent decision tree (§ 6 below).

---

### P2.2 — Optimistic UI for low-risk mutations
**Status:** ❌ Missing — no pattern doc, no helper.
**What:** For low-risk, easily-reversible mutations (like, bookmark, mark-read), update UI immediately and rollback if the server rejects. For high-risk (delete, payment), wait for confirmation.
**Why kit:** This is the single biggest "feels-fast" lever for SaaS / e-commerce kits. AI agents default to pessimistic ("wait for server") which feels slow on 4G connections. Without a decision rule, agents pick wrong.
**Authority:** 🌐 [web.dev INP](https://web.dev/articles/inp) — perceived responsiveness driver. 🌐 [LogRocket on Doherty](https://blog.logrocket.com/ux-design/designing-instant-feedback-doherty-threshold/) — "you can use perceived performance to improve response time".
**Recipe:** Decision rule encoded as comment template:
```
Optimistic if: (reversible == true) AND (failure_rate < 1%) AND (server_only_logic == false)
Pessimistic if: payment OR delete OR cross-user state OR validation server-side
```
Toast (`role="status"`) with undo button is the rollback affordance.
**Anti:** ❌ Optimistic on payment ("you've been charged" before server confirms) · ❌ Optimistic on hard-delete with no undo · ❌ Pessimistic on a like button (the entire reason likes feel fast on Twitter/IG).
**In kit:** Needs new pattern entry in INTERACTION-RULES or new file `STATE-MANAGEMENT.md`. **Gap.**

---

### P2.3 — Focus-visible (keyboard-only focus ring)
**Status:** ✅ Implemented (A11Y-RULES § 3)
**What:** Focus ring appears only on keyboard nav (Tab), not on mouse click. CSS `:focus-visible` pseudo-class handles this natively.
**Why kit:** WCAG 2.4.7 + WCAG 2.2's NEW 2.4.13 Focus Appearance (AAA). 🌐 [WCAG 2.2](https://www.w3.org/TR/WCAG22/) made target-size + focus a high-priority gap.
**Authority:** 🌐 [WCAG 2.2 Quick Ref](https://www.w3.org/TR/WCAG22/) § 2.4.13 Focus Appearance (AAA) — focus indicator min area + 3:1 contrast vs unfocused state.
**Recipe:** Kit tokens:
```css
button:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: inherit;
}
```
Contrast checker confirms primary on bg ≥ 3:1 (kit's `contrast-check-brand.js` enforces).
**Anti:** ❌ `outline: none` without replacement · ❌ Focus ring same color as element (no contrast) · ❌ Focus ring clipped by parent `overflow: hidden`.
**In kit:** 📄 A11Y-RULES.md § 3.

---

### P2.4 — Modal vs drawer vs sheet decision rule
**Status:** ⚠️ Partial — all three components exist in kit, but no decision rule doc.
**What:** Which overlay type for which task is a frequently-wrong AI agent choice. Rule needed.
**Why kit:** AI agents pick "modal" by default → over-modal-ifies the entire UX. Users hate modal fatigue. A decision table forces correct picking.
**Authority:** 🌐 [NN/G Bottom Sheets](https://www.nngroup.com/articles/bottom-sheet/): bottom sheets preserve background context, modals demand attention. 🌐 [LogRocket Sheets vs Dialogs](https://blog.logrocket.com/ux-design/sheets-dialogs-snackbars/): modals = emergencies + irreversible confirms; drawers = secondary navigation/filters; sheets = mobile-first transient.
**Recipe:** Decision tree (also see § 6.1):

| Use case | Overlay |
|---|---|
| Confirm destructive action ("Delete account?") | **Modal** (`role="dialog"` + `aria-modal=true`) |
| Onboarding gate (first-run setup) | **Modal** |
| Edit a record (form, retains background context) | **Drawer** right-side |
| Filter / sort sidebar (frequently toggled) | **Drawer** |
| Mobile menu / picker | **Bottom sheet** |
| Quick action (share, save-to) on mobile | **Bottom sheet** |
| Non-urgent status info ("Saved", "Connected") | **Toast** (`role="status"`) — NOT a modal |
| Urgent attention ("Connection lost") | **Toast** (`role="alert"`) |

**Anti:** ❌ Modal for non-destructive secondary nav · ❌ Drawer for irreversible confirm (lacks emphasis) · ❌ Sheet on desktop (treat as drawer instead) · ❌ Multiple stacked modals.
**In kit:** Tokens + components exist. **Gap:** Decision table should be inline in INTERACTION-RULES.md or in this doc § 6.1.

---

### P2.5 — Long-press / right-click context (touch + desktop parity)
**Status:** ❌ Missing
**What:** Power-user actions accessed via right-click on desktop / long-press on touch. Same menu, different gesture.
**Why kit:** Saves primary surface real-estate. Without a primitive, agents add "..." buttons everywhere (visual noise) or never expose actions (discoverability fail).
**Authority:** 🌐 [ARIA APG Menu](https://www.w3.org/WAI/ARIA/apg/patterns/) — Menu pattern with `role="menu"` + arrow-key nav.
**Recipe:** Needs new molecule `context-menu` with:
- `oncontextmenu` (desktop right-click)
- `touchstart` + 500ms timer (mobile long-press)
- ARIA `role="menu"` + arrow-key nav + Esc-close
**Anti:** ❌ Long-press without haptic feedback on mobile (user unsure it fired) · ❌ Right-click that blocks browser native menu without offering equivalent items.
**In kit:** Not present. Add to molecules.

---

### P2.6 — Hover delay (tooltip + dropdown — anti-flicker)
**Status:** ✅ Implemented for tooltip (INTERACTION-RULES § 13)
**What:** Hover-triggered overlays must wait 500–700ms before opening (anti-flicker) and 100ms before closing (lets user reach the overlay to copy text).
**Why kit:** Without delay, cursor-traversing-page triggers flashing tooltips on every hover. Fitts's Law tradeoff: too long → user thinks tooltip is broken; too short → flicker.
**Authority:** 🌐 [Fitts's Law (Laws of UX)](https://lawsofux.com/) — time to acquire target = f(distance, size). Delay accommodates "passing-through" cursor movements that aren't intentional hovers.
**Recipe:** Existing tokens — tooltip delay-open 600ms (INTERACTION-RULES § 13). Dropdown opens on **click**, not hover (avoid hover-dependent reveals — WCAG 1.4.13 Content on Hover).
**Anti:** ❌ Tooltip opens at 0ms (flicker) · ❌ Hover-only dropdown (touch users can't access) · ❌ Tooltip carries critical info (must be in text too).
**In kit:** 📄 INTERACTION-RULES.md § 13.

---

## Section 3 — Information architecture & cognition

### P3.1 — Chunking (Miller's 7±2 working memory)
**Status:** ⚠️ Partial — kit has spacing tokens but no "max items per group" guidance.
**What:** Group related items in chunks of 5–7. Beyond that, sub-group with visual separator or section header.
**Why kit:** AI agents generating UIs from data don't know to chunk — they emit one massive list. Cognitive load spikes.
**Authority:** 🌐 [Miller's Law (Laws of UX)](https://lawsofux.com/) — working memory caps at 7±2 items.
**Recipe:** Pattern rule:
```
If list length > 7: group with <section> + <h3> sub-header
If list length > 20: add filter / search input on top
If list length > 100: paginate or virtualize
```
Use `--space-6` (24px) between chunks, `--space-2` (8px) within a chunk.
**Anti:** ❌ Flat 50-item dropdown · ❌ Settings page with 30 toggles in one column · ❌ Form with 15 fields and no sections.
**In kit:** Touched in LAYOUT-SYSTEM.md spacing rhythm. **Gap:** Explicit "max items per group = 7" rule.

---

### P3.2 — Progressive disclosure (don't show everything at once)
**Status:** ⚠️ Partial — kit has accordion/disclosure components, no usage rule.
**What:** Show essential controls by default; hide advanced/secondary behind explicit toggle (accordion, "Advanced" link, drawer).
**Why kit:** Hick's Law: decision time grows with number of options. Default-hiding 80% of controls reduces decision cost on the 80% of users who don't need them.
**Authority:** 🌐 [Hick's Law](https://lawsofux.com/) — decision time ∝ log₂(N+1). 🌐 [ARIA APG Disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/) — `aria-expanded` + `aria-controls` pattern.
**Recipe:** Decision rule:
```
Show by default: fields needed for >70% of completions
Disclose behind toggle: advanced options, optional fields, expert preferences
NEVER hide: required fields, primary action, error messages
```
Use kit accordion (`aria-expanded` toggle).
**Anti:** ❌ Hiding required fields ("show more" reveals required input) · ❌ "Advanced" containing options 90% of users need · ❌ Disclosed content not anchored (scrolls off after expand).
**In kit:** Accordion atom exists. **Gap:** Usage guidance.

---

### P3.3 — Fitts's Law applied (target size + edge proximity)
**Status:** ✅ Implemented (44×44 touch target token)
**What:** Touch targets ≥ 44×44 CSS px (Apple HIG) or 24×24 with adequate spacing (WCAG 2.2 § 2.5.8). Primary action placed at screen edge or corner if possible (infinite Fitts size).
**Why kit:** Kit's primary failure mode without rule = AI emits 28px icon buttons on mobile = tap miss rate spikes.
**Authority:** 🌐 [Apple HIG Layout](https://developer.apple.com/design/human-interface-guidelines/) 44pt min · 🌐 [WCAG 2.2 § 2.5.8 Target Size Minimum](https://www.w3.org/TR/WCAG22/) — 24×24 with spacing allowance (AA).
**Recipe:** Token `--touch-min: 44px` (A11Y-RULES § 4). Primary CTA = sticky bottom on mobile, top-right on desktop.
**Anti:** ❌ Icon button without `min-width/min-height` · ❌ Touch targets < 8px apart (accidental tap) · ❌ Primary action centered on long-scroll page (Fitts max distance).
**In kit:** 📄 A11Y-RULES.md § 4. ✅ Pattern enforced.

---

### P3.4 — Serial position effect (first + last positions are remembered)
**Status:** ❌ Missing as guidance
**What:** Users best remember the first and last items in a sequence. Put highest-priority actions at the extremes of a menu/toolbar, not the middle.
**Why kit:** AI agents emit menus in arbitrary/alphabetical order. High-leverage cognitive lever.
**Authority:** 🌐 [Serial Position Effect](https://lawsofux.com/) — "users best remember the first and last items in a series".
**Recipe:** Ordering rule for menus / toolbars / nav:
```
Position 1 (first):     most-frequent action
Position N (last):      destructive / sign-out / exit action
Middle positions:       secondary / less-frequent
```
Apply to `<nav>`, dropdown menus, toolbars, settings pages.
**Anti:** ❌ "Delete account" first in settings menu · ❌ Alphabetical primary nav · ❌ Logout in the middle of profile dropdown.
**In kit:** Not encoded. Add to USAGE-GUIDELINES.md.

---

### P3.5 — Peak-end rule (design for ends and high moments)
**Status:** 🔮 Future
**What:** Users judge an experience by its peak emotional moment + how it ended. Invest disproportionately in onboarding's end (first-success), checkout's final step, and error recovery copy.
**Why kit:** This is the rare brand-overlapping pattern that has a kit-level affordance: the kit should ship "success" / "complete" components that are deliberately delightful (vs the rest of the kit which is restrained).
**Authority:** 🌐 [Peak-End Rule](https://lawsofux.com/) — Daniel Kahneman.
**Recipe:** Kit's `--ease-spring` + confetti primitive (does not exist yet) for "complete" states. Use sparingly: at flow end (purchase confirmation, task complete), not at every micro-success.
**Anti:** ❌ Confetti on every form submit (devalues) · ❌ Bland "Success" toast at flow end (missed peak) · ❌ Generic error message on terminal failure (missed end recovery).
**In kit:** No `Confetti` / `SuccessEnd` molecule yet. **Gap.**

---

## Section 4 — A11y patterns (kit-layer enforcement)

### P4.1 — Semantic HTML > ARIA (use button before role="button")
**Status:** ✅ Implemented (A11Y-RULES § 5 nguyên tắc)
**What:** Prefer native HTML elements over ARIA-decorated divs. `<button>` over `<div role="button" tabindex="0">`.
**Why kit:** Native elements ship a11y semantics + keyboard handling + focus order free. Custom-rolled equivalents always miss something.
**Authority:** 🌐 [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) — "no ARIA is better than bad ARIA" (the first rule of ARIA use).
**Recipe:** Kit components MUST use:
- `<button type="button">` for actions
- `<a href>` for navigation
- `<input type=…>` over custom widgets unless impossible
- ARIA only when native HTML has no equivalent (combobox, treegrid, tablist)
**Anti:** ❌ `<div role="button" tabindex="0" onclick=…>` (missing Enter/Space handlers, no native focus) · ❌ `<span>` with click handler.
**In kit:** 📄 A11Y-RULES.md § 5. ✅ All react/ components use native button/a.

---

### P4.2 — WCAG 2.2 new criteria coverage
**Status:** ⚠️ Partial — kit targets WCAG 2.1 AA per A11Y-RULES § 1. WCAG 2.2 brought 9 new SC in 2023 (Level AA: 2.4.11, 2.5.7, 2.5.8).
**What:** Five WCAG 2.2 SC at Level A/AA that the kit should encode:

| SC | Title | Kit applies via |
|---|---|---|
| 2.4.11 Focus Not Obscured (AA) | Focused element not entirely hidden by sticky header/etc | Z-index review; `scroll-padding-top` on `html` |
| 2.5.7 Dragging Movements (AA) | Drag-and-drop has single-pointer alternative | Slider has +/- buttons; reorder list has up/down buttons |
| 2.5.8 Target Size Minimum (AA) | 24×24 CSS px min with spacing OR 44×44 | Touch token `--touch-min: 44px` |
| 3.2.6 Consistent Help (A) | Help/contact in same location across pages | Templates pin help in footer same place |
| 3.3.7 Redundant Entry (A) | Don't re-ask info user already entered in same flow | Form state persistence pattern |

**Authority:** 🌐 [WCAG 2.2 Quick Ref](https://www.w3.org/TR/WCAG22/) · 🌐 [Vispero summary](https://vispero.com/resources/new-success-criteria-in-wcag22/).
**Recipe:**
- 2.4.11 → `html { scroll-padding-top: var(--header-h); }` for sticky headers
- 2.5.7 → Every draggable item gets keyboard equivalent (Arrow keys reorder, Space picks up)
- 2.5.8 → Touch token already enforces
- 3.2.6 → Template-level: footer "Help" link fixed position across all pages
- 3.3.7 → Form fields with `autocomplete="…"` attributes; multi-step form preserves state
**Anti:** ❌ Drag-only reorder with no keyboard fallback · ❌ Help link position varies page-to-page.
**In kit:** A11Y-RULES targets 2.1. **Gap:** Upgrade A11Y-RULES baseline to 2.2 AA.

---

### P4.3 — Live regions (polite vs assertive)
**Status:** ✅ Implemented (A11Y-RULES § 5 table)
**What:** Dynamic content updates announced to screen readers via `aria-live="polite"` (status, non-urgent) or `aria-live="assertive"` (errors, urgent).
**Why kit:** SR users miss in-flight UI changes otherwise. Kit components (Toast, FormError, Spinner) must use the right one.
**Authority:** 🌐 [ARIA APG Alert](https://www.w3.org/WAI/ARIA/apg/patterns/) + 🌐 [MDN ARIA live regions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions).
**Recipe:** A11Y-RULES § 5 table maps this. Quick:
- `role="status"` + polite → save indicators, count updates, mild form feedback
- `role="alert"` + assertive → form errors on submit, connection lost, destructive confirm
- Loading spinner → `role="status"` + sr-only "Loading"
**Anti:** ❌ `role="alert"` on every toast (becomes background noise SR-side) · ❌ Updating live region too frequently (announcement spam).
**In kit:** 📄 A11Y-RULES.md § 5. ✅ Toast atom respects this.

---

### P4.4 — Color contrast token-level enforcement
**Status:** ✅ Implemented (contrast-check-brand.js CI gate)
**What:** Every text-on-background token combination must hit 4.5:1 (body) or 3:1 (large/UI). Verified automatically on every brand preset.
**Why kit:** AI agents can author brand presets with arbitrary HSL values. Without CI, contrast violations ship.
**Authority:** 🌐 [WCAG 2.1 § 1.4.3 Contrast (Minimum)](https://www.w3.org/WAI/WCAG21/quickref/) · WCAG 2.2 maintained.
**Recipe:** CI script `_build/contrast-check-brand.js` runs on every brand.css change. v0.4.1 HSL math hardened (semantic.css comments lines 13-20).
**Anti:** ❌ Hex literals in components (bypass contrast checker) · ❌ Brand preset bypassing CI gate.
**In kit:** 📄 [_build/contrast-check-brand.js](file:///C:/Users/DANG/AI-Cowork/00-templates/ui-kit/_build/contrast-check-brand.js). ✅ Gate enforced.

---

## Section 5 — Performance UX patterns

### P5.1 — Skeleton vs spinner vs progress decision rule
**Status:** ⚠️ Partial — kit has skeleton + spinner components, but no decision rule encoded.
**What:** Three loading affordances, three contexts. AI agents pick wrong constantly.
**Authority:** 🌐 [NN/G Skeleton Screens](https://www.nngroup.com/articles/skeleton-screens/) ships exact thresholds:

| Load duration | Affordance |
|---|---|
| < 1 second | **Nothing** (animation more annoying than wait) |
| 1–10 seconds, full page | **Skeleton screen** matching final layout |
| 2–10 seconds, single component | **Spinner** on that component |
| > 10 seconds | **Progress bar** with % or eta |
| Indeterminate, no eta | **Spinner** + status text ("Connecting…") |

**Why kit:** Without rule, agents emit spinners for full-page loads (per NN/G research: 9-20% bounce-rate penalty vs skeletons).
**Recipe:** Encode this as a decision table in INTERACTION-RULES.md § 7 (currently just describes spinner mechanics). Skeleton match-layout requirement = use the SAME spacing/sizing tokens as the loaded state.
**Anti:** ❌ Spinner middle of empty page for 5s (cognitive uncertainty) · ❌ Skeleton that doesn't match loaded layout (CLS spike, perceived "redraw") · ❌ Progress bar with no eta (worse than nothing).
**In kit:** Spinner + skeleton primitives exist. **Gap:** Decision table not in docs.

---

### P5.2 — CLS prevention via reserved space
**Status:** ⚠️ Partial — kit avoids `width/height: auto` but no explicit CLS pattern doc.
**What:** Images, embeds, ads must declare width × height so browser reserves space → no layout shift when they load.
**Why kit:** Core Web Vitals CLS ≤ 0.1 at p75. Kit's reputation depends on it.
**Authority:** 🌐 [web.dev Core Web Vitals](https://web.dev/articles/vitals) — CLS thresholds: ≤0.1 good, ≤0.25 needs improvement, >0.25 poor.
**Recipe:**
```html
<img src="…" width="640" height="360" loading="lazy" alt="…">
<!-- OR -->
<div style="aspect-ratio: 16/9; background: var(--surface-2);">
  <img src="…" alt="…" loading="lazy">
</div>
```
For fonts: declare fallback face with metric-matched `size-adjust` (kit already does this — see foundation.css `--font-sans` fallback).
**Anti:** ❌ `<img>` without width/height attributes · ❌ Web font that shifts metrics on swap (use font-display:optional + size-adjust fallback) · ❌ Lazy-loading hero image (LCP penalty).
**In kit:** Font fallback handled. **Gap:** Image aspect-ratio guidance.

---

### P5.3 — Anti-flash minimum hold (rapid network case)
**Status:** ✅ Implemented (INTERACTION-RULES § 7)
**What:** When async action completes in < 100ms, the loading indicator flashes → user notices "what was that?". Solution: hold loading state min 200ms even if response faster.
**Why kit:** Counter-intuitive: faster server = worse perceived UX without hold.
**Authority:** Web.dev INP guidance + Material 3 emphasizes "perceived smoothness".
**Recipe:**
```js
const start = Date.now();
const result = await fetch(…);
const elapsed = Date.now() - start;
if (elapsed < 200) await sleep(200 - elapsed);
// then update UI
```
Or CSS-only: skeleton has min animation duration so the first frame isn't immediately replaced.
**Anti:** ❌ Removing loading state in same tick as response (flash) · ❌ Hold > 500ms (now feels artificially slow).
**In kit:** 📄 INTERACTION-RULES.md § 7. ✅ Documented.

---

### P5.4 — Lazy-load below-fold, eager above-fold
**Status:** ⚠️ Partial — kit doesn't enforce; archetype starters mix.
**What:** Images/iframes below the fold use `loading="lazy"`; above-fold (LCP candidate) loads eagerly with `fetchpriority="high"`.
**Why kit:** LCP ≤ 2.5s at p75. Eager-loading the hero image is the #1 LCP lever.
**Authority:** 🌐 [web.dev LCP](https://web.dev/articles/lcp).
**Recipe:**
```html
<!-- Above fold (hero) -->
<img src="hero.webp" fetchpriority="high" loading="eager"
     width="…" height="…" alt="…">

<!-- Below fold -->
<img src="…" loading="lazy" decoding="async"
     width="…" height="…" alt="…">
```
**Anti:** ❌ `loading="lazy"` on the LCP image (defers LCP by 200-800ms) · ❌ `fetchpriority="high"` on every image (defeats the priority).
**In kit:** Pages have mixed treatment. **Gap:** Add rule to USAGE-GUIDELINES.md.

---

## Section 6 — AI-agent UX usage patterns

> This section is unique to this kit. Primary consumer = AI agent via `/kry-ui`. Patterns formatted as decision trees the agent can apply mechanically.

### P6.1 — Overlay-type decision tree

```
START: User needs to expose a secondary surface.

Q1: Is the action destructive / irreversible / blocks all other work?
├─ YES → MODAL (role="dialog", aria-modal=true, Esc + backdrop close, focus trap)
└─ NO → Q2

Q2: Does the surface need to coexist with the background (compare, multitask)?
├─ YES → Q3
└─ NO → Q4

Q3: Mobile or desktop primary target?
├─ MOBILE → BOTTOM SHEET (drag handle, swipe-down dismiss, 85vh max)
└─ DESKTOP → DRAWER (right-side, persistent in session, Esc close)

Q4: Is the message advisory (info) or interruptive (action required)?
├─ ADVISORY → TOAST role="status" (polite, auto-dismiss 4s)
├─ INTERRUPTIVE → TOAST role="alert" (assertive, requires user dismiss or 6s+ timeout)
└─ FORM ERROR → INLINE error (aria-describedby), NOT a toast
```

### P6.2 — Loading-state decision tree

```
START: An async operation begins.

Q1: Expected duration < 1s on p50 connection?
├─ YES → NO INDICATOR. Just paint result. (Animation < 1s is noise.)
└─ NO → Q2

Q2: Is this a full-page load or a single component?
├─ FULL PAGE → SKELETON SCREEN (must match final layout)
└─ COMPONENT → Q3

Q3: Is duration determinate (you know % progress)?
├─ YES → PROGRESS BAR with % + label
└─ NO → SPINNER + status text ("Connecting…", "Saving…")

Q4: Will operation likely complete in < 200ms (rapid network)?
├─ APPLY ANTI-FLASH: setTimeout to ensure indicator visible min 200ms
```

### P6.3 — Form validation timing decision tree

```
START: Form field needs validation.

Q1: Is this the field's FIRST interaction (user hasn't blurred yet)?
├─ YES → NO error displayed. Validate silently.
└─ NO (field was touched) → Q2

Q2: Is the field currently in error state from previous validation?
├─ YES → Validate ON EVERY CHANGE (reward early — show fix immediately)
└─ NO → Q3

Q3: Did the user just BLUR (leave) the field?
├─ YES → Validate now. Show error if invalid.
└─ NO (still typing) → Wait. Don't error during typing.

Q4: On form SUBMIT:
├─ Validate ALL fields synchronously
├─ Focus first invalid field
├─ Announce error count via aria-live="assertive"
```

**Authority:** 🌐 [Polaris Inline Error](https://polaris-react.shopify.com/components/selection-and-input/inline-error) — "removed as soon as the input is valid". This implements **reward-early-punish-late** (errors stay quiet until earned, fixes celebrated immediately).

### P6.4 — Animation easing decision tree

```
START: Element needs a transition.

Q1: Is this routine UI (hover, dropdown, modal) or a delight moment (success, achievement)?
├─ ROUTINE → Q2
└─ DELIGHT → --ease-spring (occasionally) or --ease-bounce (rarely)

Q2: Is the element entering or exiting?
├─ ENTERING → --ease-out (decelerate — feels like arrival)
├─ EXITING → --ease-out (acceleration variant — feels decisive)
   [v0.6 future: add --ease-enter / --ease-exit pair per P1.2]

Q3: Is it a continuous loop (spinner, progress fill)?
├─ YES → --ease-linear (only place linear is correct)
└─ NO → --ease-out
```

### P6.5 — Empty / error / loading state matrix (every data component must implement)

Every component that displays remote data MUST handle 4 states:

| State | What renders | Pattern |
|---|---|---|
| Loading first-time | Skeleton matching final layout | P5.1 |
| Loaded with data | Content | — |
| Loaded but empty | Empty state with icon + 1-sentence explanation + CTA | "No bookings yet — Add your first" |
| Error | Error message + retry button + (optional) help link | "Couldn't load — Retry" |

**Anti:** ❌ Component that shows spinner forever when API errors (no error UI) · ❌ Empty state that looks identical to error state (user confusion) · ❌ Empty state with no CTA ("dead end").

---

## Section 7 — UX gap matrix vs industry standards

How does the kit's UX layer compare to four named production design systems?

| Dimension | Cowork v0.5.9 | Material 3 | Apple HIG | Polaris (Shopify) | Carbon (IBM) |
|---|---|---|---|---|---|
| **Duration token scale** | ✅ 5-step (instant/fast/normal/slow/slower) | ✅ 16-step (short1-4, medium1-4, long1-4, xlong1-4) | ⚠️ Guidelines only, no formal tokens | ✅ 3-step | ✅ 7-step (productive vs expressive split) |
| **Easing curve library** | ✅ 6 named curves | ✅ 9 named curves (incl emphasized enter/exit) | ⚠️ Implicit "natural" guidance | ✅ Implicit | ✅ Standard + entrance + exit per motion mode |
| **Enter/exit asymmetric easing** | ❌ Single `--ease-out` default | ✅ Emphasized accelerate/decelerate split | ✅ "Arrival decelerates" | ⚠️ Not formalized | ✅ Productive entrance vs exit tokens |
| **Stagger / choreography primitive** | ❌ Not in kit | ✅ Documented + token | ✅ "Coherent motion" principle | ❌ | ✅ Choreography doc |
| **Reduced-motion override** | ✅ Global rule | ✅ | ✅ | ✅ | ✅ |
| **Focus-visible (kbd-only ring)** | ✅ | ✅ | ✅ | ✅ | ✅ |
| **WCAG 2.2 SC coverage** | ⚠️ Targets 2.1 AA, partial 2.2 | ✅ 2.2 AA | ✅ | ✅ | ✅ |
| **Touch target ≥ 44px** | ✅ Token enforced | ✅ 48dp | ✅ 44pt | ✅ | ✅ |
| **Modal / drawer / sheet decision rule** | ⚠️ Components exist, no decision tree | ✅ Doc'd | ✅ Doc'd | ✅ Doc'd | ⚠️ |
| **Skeleton vs spinner rule** | ⚠️ Components exist, no rule | ✅ | ⚠️ | ✅ | ⚠️ |
| **Optimistic UI pattern** | ❌ Not documented | ⚠️ Implied | ⚠️ | ✅ Doc'd | ⚠️ |
| **Form validation timing (touched, blur, submit)** | ❌ Not documented | ⚠️ | ⚠️ | ✅ Doc'd | ⚠️ |
| **Live regions (polite/assertive)** | ✅ A11Y-RULES table | ✅ | ✅ | ✅ | ✅ |
| **CI gates for a11y + contrast** | ✅ axe + contrast-check-brand | — | — | ⚠️ | ⚠️ |
| **AI-agent decision trees** | ✅ This document § 6 | ❌ (human-targeted) | ❌ | ❌ | ❌ |
| **Visual proof regression CI** | ✅ visual-diff.js 3-tier | — | — | ⚠️ | ⚠️ |

**Honest summary:** Kit is **Polaris-shaped** (token-driven, CI-gated, opinionated, single-team) but with a Material-3-flavored motion vocabulary. Closest production analogue = **Polaris**. Biggest deltas from production-grade:

1. **Motion choreography depth** — Material 3 has 16-step durations + emphasized enter/exit pair; kit has 5-step single-curve default.
2. **Optimistic UI + validation timing** — Polaris ships explicit rules; kit silent on both.
3. **AI-agent affordances** — but kit is *ahead* here (§ 6 decision trees are not in any of the named systems because their consumer is a human designer, not an AI agent).

---

## Section 8 — Brand voice as a token layer (future, out of scope)

The kit deliberately treats "brand voice" (tone of copy, motion personality, density preference) as a future token layer that overlays the kit's brand-agnostic foundation. Hooks for this exist:

- **Motion personality token** (future): `--motion-personality: subtle | balanced | expressive` could pick between `--ease-out` (subtle), `--ease-out-soft` (balanced), and `--ease-spring`-based (expressive) as defaults.
- **Density token** (future): `--density: compact | comfortable | spacious` could multiply `--space-*` and `--touch-min`.
- **Copy tone token** (future): `--tone: warm | professional | playful` could swap default labels in shared components (e.g. "Saved!" vs "Saved successfully" vs "Got it ✨").

None of these are implemented in v0.5.9. They are noted here so future brand kits don't re-invent the layer.

**This document does NOT define KHI's calm tone vs BetterBuy's energetic tone.** Those are brand-layer concerns to be addressed in `brands/<brand>.brand.css` extensions or per-product UX docs.

---

## References (authoritative sources cited)

| # | Source | URL |
|---|---|---|
| 1 | Material 3 — Easing & Duration Tokens | 🌐 https://m3.material.io/styles/motion/easing-and-duration/tokens-specs |
| 2 | Material Foundation — motion.json (exact values) | 🌐 https://github.com/material-foundation/material-tokens/blob/json/json/motion.json |
| 3 | Material 3 Expressive — Motion blog post | 🌐 https://m3.material.io/blog/m3-expressive-motion-theming |
| 4 | Apple HIG — Motion | 🌐 https://developer.apple.com/design/human-interface-guidelines/motion |
| 5 | WAI-ARIA Authoring Practices Guide | 🌐 https://www.w3.org/WAI/ARIA/apg/ |
| 6 | WAI-ARIA APG — Pattern list (30 patterns) | 🌐 https://www.w3.org/WAI/ARIA/apg/patterns/ |
| 7 | WCAG 2.2 | 🌐 https://www.w3.org/TR/WCAG22/ |
| 8 | WCAG 2.1 Quick Reference (AA baseline) | 🌐 https://www.w3.org/WAI/WCAG21/quickref/ |
| 9 | web.dev — Interaction to Next Paint (INP) | 🌐 https://web.dev/articles/inp |
| 10 | web.dev — Web Vitals (LCP/CLS/INP thresholds) | 🌐 https://web.dev/articles/vitals |
| 11 | NN/G — Skeleton Screens 101 | 🌐 https://www.nngroup.com/articles/skeleton-screens/ |
| 12 | NN/G — Bottom Sheets UX Guidelines | 🌐 https://www.nngroup.com/articles/bottom-sheet/ |
| 13 | Laws of UX (Jon Yablonski) | 🌐 https://lawsofux.com/ |
| 14 | Doherty Threshold (400ms productivity ceiling) | 🌐 https://lawsofux.com/doherty-threshold/ |
| 15 | Carbon Design System — Motion overview | 🌐 https://carbondesignsystem.com/elements/motion/overview/ |
| 16 | Carbon — Motion Choreography | 🌐 https://carbondesignsystem.com/elements/motion/choreography/ |
| 17 | Shopify Polaris — Inline Error component | 🌐 https://polaris-react.shopify.com/components/selection-and-input/inline-error |
| 18 | Vispero — WCAG 2.2 New SC summary | 🌐 https://vispero.com/resources/new-success-criteria-in-wcag22/ |
| 19 | LogRocket — Sheets vs Dialogs vs Snackbars | 🌐 https://blog.logrocket.com/ux-design/sheets-dialogs-snackbars/ |
| 20 | MDN — ARIA Live Regions | 🌐 https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions |

---

## Changelog

- **2026-05-20 · v0.1** — Initial kit-layer UX patterns doc. 5 sections (motion, interaction, IA & cognition, a11y, performance) + § 6 AI-agent decision trees + § 7 gap matrix vs Material 3 / Apple HIG / Polaris / Carbon. 20 authoritative sources cited. Brand-voice section explicitly future-scoped (§ 8).
