| 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 โ removed 2026-07-25. No suite drives BL-UI-001..006 against the live storefront any more; this skill is now the only executor of those invariants. Work from the audit protocols + coverage matrix in critical-ui-scope.md (all cells GAP).
- 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.