| name | qa-design |
| description | [Testing] Design system consistency & UX heuristics: live-token audit, BL-UI invariants, Nielsen's 10, Figma comparison. |
| argument-hint | component | page URL | flow name |
/qa-design — Design & UX Evaluation
Terminal command: /qa-design <component | page | flow> — see commands/qa-design.md. The command parses the argument, resolves audit scope from critical-ui-scope.md, and dispatches ui-ux-expert; this file is the methodology library it consults.
Validate design system consistency and run UX heuristic evaluations against the active Coffee theme tokens, BL-UI invariants, and Nielsen's 10 usability heuristics.
Usage
/qa-design ProductCard # Audit one component against the design system
/qa-design checkout flow # UX heuristic evaluation of a flow
/qa-design https://figma.com/... # Compare implementation vs Figma spec (see Figma section below)
Pre-requisites — read these first
Before any design audit, the agent must already be aware of:
business-logic.md Domain 15 (BL-UI-001..006) — the canonical UI invariants. A violation is a FAIL regardless of how the result looks.
scripts/lib/measure-layout.ts — the helper that wraps every required measurement: CLS observer, spacing audit, alignment audit, overflow audit, touch-target audit, rect snapshot for shift detection, occlusion audit (BL-UI-007), text contrast audit (BL-UI-008, WCAG 1.4.3), non-text/icon contrast audit (nonTextContrastAuditSnippet, WCAG 1.4.11), focus-indicator audit (BL-UI-009), image aspect-ratio audit (BL-UI-010), sized-control token+aspect audit (sizedControlAuditSnippet, VCST-5413), alert-semantics audit (alertSemanticsAuditSnippet, WCAG 4.1.3), plus classifiers.
048b-layout-stability.csv — the 18-case suite that drives BL-UI-001..006 against live storefront. Run via SUITE_SELECTION=layout-stability npm run ci:regression.
- storefront-config-flags.md — active theme preset + flags affecting which tokens render.
- Proposed BL-UI-007..010 (occlusion, contrast, focus-indicator, image aspect-ratio) + a BL-UI-001 refinement are implemented as audits in
measure-layout.ts but are not yet promoted into business-logic.md Domain 15 (which currently defines only BL-UI-001..006). Audit with them; cite them as PROPOSED-BL-UI-NNN until promoted. The non-text-contrast (icon/graphic, WCAG 1.4.11 — classifier invariant BL-UI-008-NONTEXT), sized-control (SIZED-CONTROL), and alert-semantics (WCAG-4.1.3) audits are newer additions covering gaps the text/occlusion snippets miss — cite them by their WCAG/VCST id.
Supporting Files
- design-system-consistency.md — Live-token extraction protocol (replaces the old hardcoded palette); spacing/color/typography/border/icon/animation audits; findings → filings decision tree. Pinned to BL-UI-002 and BL-UI-005.
- ux-heuristic-evaluation.md — Nielsen's 10 with Coffee/B2B-specific examples; Nielsen 0–4 severity rubric; heuristic → BL-* / WCAG / ECL cross-reference table.
Execution
Delegate to ui-ux-expert via the Agent tool (subagent_type: ui-ux-expert). The skill itself does not run measurements — the agent does, using the canonical helper.
Design System Consistency Check
- Extract live tokens from the page under test (NOT from hardcoded values in this skill):
const tokens = getRootCustomProperties();
- Audit spacing with
spacingAuditSnippet(selector) from measure-layout.ts, classify with classifySpacing(). Pin every finding to BL-UI-002.
- Audit alignment with
alignmentAuditSnippet(selector) + classifyAlignment(). Pin to BL-UI-005.
- Audit color usage + WCAG 1.4.3 contrast: every brand-styled element must reference
var(--color-…), not a literal hex. Run contrastAuditSnippet(selector) + classifyContrast() to enforce 4.5:1 / 3:1 ratios. Pin to PROPOSED-BL-UI-008 (or BL-UI-008 once promoted). Toggle theme preset — literals don't move, tokens do; a contrast PASS on default may FAIL on a dark preset.
- 4a. Non-text / icon contrast (WCAG 1.4.11):
contrastAuditSnippet only sees TEXT nodes — it skips icons entirely. For any surface with icon glyphs, ALSO run nonTextContrastAuditSnippet(selector) + classifyNonTextContrast() (default selector targets svg / .vc-icon). It resolves each glyph's painted color (stroke for outline icons, fill for solid, else CSS color) vs background at the 3:1 minimum and exempts icons inside disabled/aria-disabled controls (WCAG 1.4.11 excludes inactive components — don't false-positive, per the VCST-5100 lesson). Cite WCAG 1.4.11 / classifier BL-UI-008-NONTEXT. This is the audit that catches the outline-first thin-muted-stroke regression (VCST-4400: enabled icons at 2.52:1).
- Audit typography: same
font-family family across surfaces, weights ∈ {400, 500, 600, 700}, body ≥ 14 px.
- Audit overflow + touch targets with
LAYOUT_SNIPPETS.overflowAudit + LAYOUT_SNIPPETS.touchTargetAudit. Pin to BL-UI-004 and BL-UI-006.
- Audit critical-alert occlusion with
occlusionAuditSnippet() + classifyOcclusion() on any page that can render .vc-alert--danger / .vc-alert--warning / [role="alert"]. Mandatory whenever the State-Stress Pass (below) renders a disabled / error / unavailable state. Pin to PROPOSED-BL-UI-007. P0 if severe: true on any overlap. When the message lives OUTSIDE the default alert set (VCST-4400: the cart over-stock warning renders in div.vc-line-item__after), pass the custom container selector: occlusionAuditSnippet(['.vc-line-item__after', ...]).
- 7a. Alert semantics (WCAG 4.1.3): a warning that is styled like an alert but carries no
role="alert"/status/aria-live is never announced to a screen reader. Run alertSemanticsAuditSnippet(selector) + classifyAlertSemantics() on suspected message slots (default targets .vc-alert--danger/--warning, line-item__after, [class*="error"]/[class*="warning"]). Advisory WARN — confirm the flagged element is a genuine status message before filing. Cite WCAG 4.1.3 / classifier WCAG-4.1.3. (Surfaced by VCST-4400: the over-stock message is not in a live region.)
- Audit focus indicators (WCAG 2.4.7) with
LAYOUT_SNIPPETS.focusIndicatorAudit + classifyFocusIndicator(). Mandatory on /sign-in, /sign-up, /cart, /checkout/payment (revenue-critical keyboard flows). Pin to PROPOSED-BL-UI-009. The snippet now skips disabled controls and separates confirmed missing from indeterminate (a programmatic .focus() often doesn't trigger :focus-visible, where themes put the ring). indeterminate items make the classifier return WARN, not FAIL — confirm them with a real keyboard-Tab pass before filing (a scripted focus that shows no ring is NOT proof of a missing ring; VCST-4400 hit 29 such false positives).
- Audit image aspect ratios with
imageAspectAuditSnippet(selector) + classifyImageAspect() on pages with product images, hero banners, logos, or CMS imagery. Pin to PROPOSED-BL-UI-010.
- Figma comparison (optional) — see Figma section below.
State-Stress Pass — audit each state, not just the default
The default render (signed-in happy-path) covers only ~30% of real-user states. Bugs frequently appear only when a page is in a non-default state — and invariant audits against the default state silently miss them. F-CART-006 (save-for-later icon covering "product no longer available" alert) was missed by a default-state-only audit because the alert only appears on disabled products.
For every page audit, enumerate and audit each applicable state separately:
| State | Trigger | Audits to re-run |
|---|
| Loading / skeleton | Throttle network (Fast 3G) or block xAPI response | BL-UI-001 (CLS), BL-UI-001 refinement (skeleton-count parity) |
| Empty | Sign in to account with no orders / wishlists / cart items | BL-UI-002 (empty-state copy spacing), BL-UI-006 (CTA size) |
| Error / disabled | Cart with a disabled / unavailable product (Capri-Sun, SHOT in vcst-qa); order with payment-failed status | BL-UI-007 occlusion (pass the message's own selector if outside the default alert set), WCAG 4.1.3 alert-semantics (is the warning announced?), BL-UI-004 overflow on alert text |
| Multi-item / overflow | Cart with 10+ line items; order with long product titles; address with long company name | BL-UI-004 overflow, BL-UI-005 alignment |
| Form-validation error | Submit form with invalid email / empty required / weak password | BL-UI-003 state-shift (validation message insertion), BL-UI-004 (long error text wrap) |
| Auth-required action when anonymous | Click "Place Order" / "Save list" while signed out | BL-UI-003 (modal insertion), BL-UI-006 (modal CTA size) |
| Dark theme | Toggle theme preset to a dark preset | BL-UI-008 text contrast, WCAG 1.4.11 non-text/icon contrast, BL-UI-009 focus visibility |
Skip a state only if the page cannot enter it (e.g., /sign-up has no "empty cart" state). When a state is skipped, record the reason in the report.
Visual-Review Screenshot Pass — supplement the snippets
Snippets measure numeric thresholds. Some defects are obvious in a screenshot but trip no measurement:
- Z-occlusion (covered by BL-UI-007 once we have alert + sibling rects, but not always — sticky-header overlapping content, footer covering content below the fold).
- Text rendering oddity (FOIT/FOUT, font-weight too light to read at small size).
- Visual rhythm break (vertical spacing between cards inconsistent within the same list — even when each card's individual spacing is on-grid).
- Brand drift (button on
/sign-in uses Coffee primary; same button on /checkout uses an off-spec hue).
After running all invariant snippets, the agent must:
- Take a baseline screenshot per viewport (375 / 768 / 1280) of the page at default state — even when all snippets PASS.
- Take a state-stress screenshot per non-default state entered in the State-Stress Pass.
- Visually review each screenshot before exiting — flag any defect not covered by an invariant snippet under a "Visual Findings" section in the report, severity-tagged by judgment with cross-reference to the closest BL-* or
PROPOSED-BL-* if one applies.
Visual-review findings are filed individually (one bug per defect) and should explicitly note "caught by visual review, not invariant snippet" so the methodology gap is visible — over time, recurring visual findings become candidates for new invariants.
Sized-Control Measurement Pass — assert against the token + aspect ratio, not a threshold
Raw dimensions that clear a "≥ threshold / no-overflow" gate can still be wrong: a control can pass full-width, touch-target ≥ 44 px, and no-overflow while rendering the wrong shape or the wrong size. VCST-5413 (VcSlider price-filter handles rendering as 34×28 ovals instead of an 18×18 circle — nouislider's default .noUi-handle winning the storefront cascade over the component's size-[--handle-size]) passed every threshold gate and was missed on the first pass, because the oracle direction was wrong: the handle's 34×28 was in the measurement table the whole time, read as "just larger" instead of "off-token and non-square."
Helper: run sizedControlAuditSnippet(selector, opts) + classifySizedControl() from measure-layout.ts — it encodes the oracle below. opts.tokenVar resolves the expected px FROM the element (e.g. { tokenVar: '--handle-size' }), or pass opts.expectedPx; opts.square (default true) or opts.ratio sets the aspect check. Run it on BOTH surfaces and diff. Classifier invariant SIZED-CONTROL.
For any control with a declared size (slider handles, avatars, icon buttons, badges, thumbnails, chips, swatches, checkboxes/radios — anything backed by a --*-size token, size-[…], or an intended square / circle / fixed-ratio shape), the oracle is equality + aspect, not a lower bound:
- Token equality. Resolve the declared design token and assert the rendered dimension equals it —
getComputedStyle(el).width === <resolved --token> (e.g. --handle-size → 18 px). "Bigger is fine" is NOT acceptable; a size that drifts off its token is a FAIL to investigate, not a footnote.
- Aspect ratio. For a control meant to be circular/square, assert
rect.width === rect.height; for a fixed-ratio control assert width / height === <intended ratio>. 34 !== 28 on a border-radius: 9999px box → oval → instant FAIL.
- Cross-surface equality. Measure the SAME control in Storybook (isolated) AND on the storefront (integrated). Any part whose size changes between isolation and integration is a red flag for a CSS-cascade override — the exact class of bug dual-eval exists to catch. Do not rationalize the drift as "an intentional skin"; treat it as a FAIL until proven a deliberate override.
- Always capture one confirmation screenshot of the integrated control — even when the numbers PASS. Numbers that clear thresholds can hide a shape that is obviously wrong to the eye; one screenshot of the real, composed control makes an oval-where-a-circle-belongs immediately visible. This single per-control confirmation shot is a deliberate exception to the FAIL-only screenshot default and stays within the per-scope budget (
evidence-capture-policy.md "final state if critical").
UX Heuristic Evaluation
- Navigate the target flow using playwright-chrome / firefox / edge MCP — see browser assignments.
- Walk Nielsen's 10 — questions and concrete VC examples in ux-heuristic-evaluation.md.
- Rate severity 0–4 per finding using the rubric in
ux-heuristic-evaluation.md (0 = not an issue, 4 = catastrophe). Promote one level on revenue-critical surfaces (checkout / payment / add-to-cart / registration).
- Cross-reference each finding against BL-*, WCAG, or ECL where one applies — a citable rule beats a subjective complaint. See the cross-ref table in
ux-heuristic-evaluation.md.
Figma comparison
Figma MCP integration is shaky in this project — the configured figma-remote-mcp only exposes authenticate / complete_authentication tools right now. If you need to compare against a Figma frame:
- If Figma MCP is fully connected (post-OAuth, paid seat): use the official server's design-context fetch to read variables + frame layout, then diff against the live computed styles. Useful for component-level spec parity.
- If not connected (current default): treat Figma URLs as manual references — open in browser, capture screenshots, eyeball-compare against the implementation. Do NOT block the audit on missing Figma data; the BL-UI invariants stand on their own.
- Free tier caveat: Figma's Starter plan caps MCP at ~6 tool calls/month — unusable for QA. Either skip Figma comparison or budget a Dev/Full seat.
See "Recommended workflow" in the project conversation log (or ask /claude-code-guide for the current state of Claude Code ↔ Figma) for the up-to-date picture.
Output
- Design consistency report — pass/fail per token category, each failure tagged with
BL-UI-NNN or PROPOSED-BL-UI-NNN and the computed-style evidence
- UX heuristic scorecard — Nielsen 0–4 rating per heuristic with specific issues, cross-referenced to BL-* / WCAG / ECL where applicable
- State-Stress matrix — per-state PASS/FAIL grid covering the states enumerated in the State-Stress Pass; skipped states must include a reason
- Visual Findings — defects spotted in the Visual-Review Screenshot Pass that no invariant snippet caught; each carries a "caught by visual review" tag so methodology gaps stay visible
- Sized-control table — for every declared-size control audited: design-token value vs rendered width×height, the aspect-ratio check, and the Storybook-vs-storefront cross-surface comparison; each row carries a confirmation screenshot of the integrated control (pass or fail)
- Screenshots — FAIL states (per
evidence-capture-policy.md) + baseline screenshots per viewport per state to support the Visual-Review Pass + one confirmation screenshot of each integrated sized control even on PASS (Sized-Control Measurement Pass)
Findings → Filings
Audits produce 0–N findings. Decision tree for what to file:
| Pattern | What to file |
|---|
| One component, one violation | Individual bug via /qa-bug tagged with the violated BL-UI-NNN |
| One component, multiple violations | ONE bug per component listing all violations — don't fragment |
| Multiple components share the same violation (5+ components with off-token color, etc.) | ONE rollup bug describing the systemic drift. Title: Design System Drift — [violation type] across [N] components. Priority bumped to P1 |
| Token resolution itself is broken | P1 bug — the design system layer is broken, not the components |
| UX heuristic finding with severity 0–1 | Log in evaluation report, do not file individually |
| UX heuristic finding with severity 2 | File as P3 bug, include suggested improvement |
| UX heuristic finding with severity 3–4 | File as P0–P1 bug, escalate severity 4 to qa-lead-orchestrator |
Rules
- Read live tokens, never hardcode — Coffee is multi-preset (6 light + 3 dark variants); a token's resolved value varies per preset. Hardcoded hex values in this skill or in audits will be wrong half the time.
- Cite the BL / WCAG / ECL ID for every finding where one applies. Findings without citations decay into vague design debate. For proposed-but-not-promoted invariants (BL-UI-007..010), cite as
PROPOSED-BL-UI-NNN; their audit logic lives in scripts/lib/measure-layout.ts until promoted into business-logic.md Domain 15.
- Audit at multiple viewports — 375 / 768 / 1280 minimum; some tokens override at breakpoint boundaries.
- Audit at multiple states — run the State-Stress Pass; default-only audits miss state-specific defects (F-CART-006 was missed precisely because the default state had no disabled product visible).
- Always run the Visual-Review Screenshot Pass before exiting — invariant snippets are necessary but not sufficient.
- For any sized control, assert token equality + aspect ratio, never a threshold — a dimension that clears "≥ / no-overflow" gates can still be the wrong size or shape (VCST-5413: 34×28 oval slider handle vs 18×18 circle). Use
sizedControlAuditSnippet(selector, {tokenVar\|expectedPx, square\|ratio}) + classifySizedControl(): assert rendered === token and width === height (or the intended ratio); cross-check Storybook vs storefront (size drift between the two = cascade-override red flag); and capture one confirmation screenshot of the integrated control even on PASS.
- Icons need the non-text-contrast audit, not the text one —
contrastAuditSnippet skips glyphs; run nonTextContrastAuditSnippet() (WCAG 1.4.11, 3:1, disabled-exempt) for any icon-bearing surface. A warning styled like an alert but with no role=alert/aria-live is unannounced — check with alertSemanticsAuditSnippet() (WCAG 4.1.3).
- A scripted-focus miss is not a focus-ring failure —
focusIndicatorAudit indeterminate items (where :focus-visible didn't trigger) are WARN, not FAIL; confirm with a real keyboard-Tab pass before filing (VCST-4400 lesson).
- UX heuristic findings ≥ 3 must be filed as bugs (P1 or higher).
- Figma comparison is optional — don't block an audit waiting for Figma access; BL-UI invariants are the authoritative contract.
- Delegate execution to
ui-ux-expert via the Agent tool (subagent_type: ui-ux-expert) — this skill is a methodology library, not an executor.