Design QA
Binary pass/fail quality gates for UI code. Run against any component, page, or feature before shipping. Report ONLY failures — silence = quality.
Fast scan (<1s, requires rg, optional sg):
bash design-qa/scripts/design-scan.sh [dir] --fix # fix suggestions
bash design-qa/scripts/design-scan.sh [dir] --json # CI output
bash design-qa/scripts/design-scan.sh [dir] --no-ui # skip components/ui/
bash design-qa/scripts/design-scan.sh [dir] --critical-only
Deep review: Read code → run gates in order → report failures with file:line and fix.
Gate 1: Anti-Slop (Critical)
ANY = immediate flag:
| Check |
Pass Condition |
| No purple-blue gradients |
Zero purple/blue/violet gradient combos |
| No gradient text |
Zero bg-clip-text text-transparent bg-gradient-* on headings/metrics |
| No glassmorphism |
Zero decorative backdrop-blur (functional overlay blur OK) |
| No hero metric template |
No big-number-in-card × 3-4 identical row pattern |
| No identical card grid |
Cards have varied content/layout |
| No glow effects |
Zero colored box-shadow spread |
| No nested cards |
Zero Card inside Card |
| Font is intentional |
Not Inter/Roboto/Arial as default |
Gate 2: Typography
| Check |
Verify |
| Project type scale only |
Grep text-[*px], text-[*rem], font-size:. Only project-defined sizes. Exception: text-[11px] micro. |
| No letter-spacing mods |
Grep tracking-*, letter-spacing. Zero unless in design spec. |
| Numeric data: tabular-nums |
All <td>/<th> with numbers, prices, counts, dates use tabular-nums. |
| Headings: text-balance |
All <h1>-<h6> use text-balance or text-pretty. |
| Line length controlled |
Body text has max-w-prose or 45-75ch equivalent. |
| Max 3 font weights |
Count distinct font-* weights. >3 = flag. |
Gate 3: Color
| Check |
Verify |
| No hardcoded colors |
Grep hex #[0-9a-f], rgb(, hsl(, oklch( in JSX. All from tokens. |
| No pure black/white |
bg-black, bg-white, #000, #fff on containers = flag. |
| No gray on colored bg |
text-gray-* on non-neutral background = flag. Use bg shade. |
| Status colors semantic |
Success=green, Error=red, Warning=amber, Info=blue. Inversion = flag. |
| Contrast ≥ 4.5:1 |
OKLCH ΔL ≥ 0.4 body, ≥ 0.3 large text. |
| Max 2 accent colors |
Count distinct accent/brand colors per view. >2 = flag. |
Gate 4: Spacing
| Check |
Verify |
| No magic numbers |
Grep p-[*], m-[*], gap-[*] with non-4px-multiple values. |
| Semantic hierarchy |
Sections > groups > items spacing, consistent within page. |
| No triple responsive padding |
p-2 md:p-4 lg:p-6 xl:p-8 = flag. One value per context. |
| 4px grid |
All spacing multiples of 4px (0.25rem). |
Gate 5: Component Reuse
| Check |
Verify |
| Existing primitives used |
Custom <button>, <input>, <dialog> when project has equivalents = flag. |
| No primitive mixing |
Multiple UI lib imports (Radix + Base UI + Headless) in same file = flag. |
| CVA for variants |
Inline ternary chains for 3+ style variants instead of CVA = flag. |
| data-slot attributes |
Missing data-slot on component roots (if project convention) = flag. |
Gate 6: Interaction States
| Check |
Verify |
| Hover on all buttons |
Every <Button>/<button> has hover styles. |
| Focus visible everywhere |
:focus-visible ring on all interactive elements. NEVER outline: none without replacement. |
| Disabled state real |
disabled:opacity-* + disabled:pointer-events-none. Not just visual. |
| Loading states exist |
Async actions show indicator. Buttons disable during submit. |
| Icon buttons: aria-label |
Every icon-only button has aria-label. |
| Destructive → AlertDialog |
Delete/discard/overwrite use AlertDialog, not Dialog/confirm. |
| Errors inline |
Shown next to trigger, not only toast/banner. |
| Paste not blocked |
No onPaste={e => e.preventDefault()}. |
Gate 7: Accessibility
| Check |
Verify |
| Semantic HTML |
<button> actions, <a> navigation, <nav>/<main>/<header>. No <div onClick>. |
| Heading hierarchy |
h1→h2→h3 in order, no skips. One h1/page. |
| Alt text |
Every <img> has alt. Decorative: alt="". |
| Form labels |
Every input has <label> (htmlFor or wrapping). Not just placeholder. |
| Keyboard nav |
Logical tab order. No traps. All functionality keyboard-reachable. |
| ARIA live regions |
Dynamic updates use aria-live="polite" or role="status". |
| Color not sole indicator |
Status paired with icon/text/pattern, never color alone. |
| Touch targets ≥ 44px |
min-h-11 min-w-11 or equivalent on mobile. |
| prefers-reduced-motion |
All animations respect it. |
| h-dvh not h-screen |
Grep h-screen. Replace with h-dvh. |
Gate 8: Edge Case Hardening
| Check |
Verify |
| Empty states designed |
Every list/table/grid has message + CTA. Not blank. |
| Long text handled |
truncate, line-clamp-*, or break-words on names/titles. |
| Overflow prevented |
Flex children: min-w-0. Grid children: min-w-0 min-h-0. |
| Numbers formatted |
Intl.NumberFormat for large numbers. Not raw digits. |
| Dates formatted |
Intl.DateTimeFormat or relative. Not raw ISO. |
| Skeletons match layout |
Skeleton shapes mirror actual content structure. |
| Double-submit prevented |
Submit buttons disable during async. |
| Error recovery |
Every error state has retry or clear path forward. |
Gate 9: Performance
| Check |
Verify |
| No layout animations |
Grep animate-*/transition-* on width/height/top/left/margin/padding. Only transform+opacity. |
| No transition-all |
Grep transition-all. Must specify: transition-colors, transition-transform, transition-opacity. |
| No permanent will-change |
will-change only within animation scope. |
| No animated blur |
backdrop-blur + transition/animate together = flag. |
| Images lazy loaded |
Below-fold: loading="lazy". |
| No layout shift |
Async content has reserved space. |
| Lists virtualized |
100+ items use virtual scrolling. |
| Inputs debounced |
Search/filter: 200-300ms debounce. |
| Cleanup on unmount |
useEffect returns cleanup (cancel subs, abort fetch, remove listeners). |
| No render-logic useEffect |
useEffect deriving values from props/state that could be computed during render = flag. |
| No console.log |
Grep console.log/warn/error not in dev check. |
Gate 10: Responsive
| Check |
Verify |
| No horizontal scroll |
Mobile layout reflows to single column. |
| No hidden core features |
hidden md:block on essential functionality = flag. |
| Logical CSS properties |
margin-inline-start/end not margin-left/right. |
| No fixed text widths |
w-24/w-[200px] on text containers = flag. |
| Zoom not disabled |
No user-scalable=no/maximum-scale=1. WCAG violation. |
| Input method aware |
@media (pointer: coarse) for touch targets. |
Gate 11: Error Resilience
| Check |
Verify |
| API errors differentiated |
401→login, 403→permission, 404→not found, 429→rate limit, 500→generic. |
| Error boundaries |
React boundaries around major sections. One crash ≠ full page down. |
| Optimistic rollback |
Failed optimistic updates revert state + show error. |
Report Format
Output ONLY failures:
## QA Report: [Component/Page]
### Critical — [Gate]: [Check] — File:Line — Issue and fix
### High — [Gate]: [Check] — File:Line — Issue and fix
### Medium — [Gate]: [Check] — File:Line — Issue and fix
Summary: X critical, Y high, Z medium | Top pattern: [most repeated]
Severity: Critical = a11y violations, anti-slop, broken states. High = missing loading, hardcoded colors, no empty states. Medium = typography, text-balance, debounce.
Quick-Check (5 gates for PR review)
- No anti-slop (Gate 1)
- No hardcoded colors (Gate 3)
- Focus visible (Gate 6)
- Empty states exist (Gate 8)
- No layout animations (Gate 9)
All pass → probably fine. Any fail → full gate sequence.
1---2name: design-qa-23description: Universal UI quality gates — accessibility, consistency, hardening, performance, responsive checks. Pre-ship checklist. Works with any AI agent, any framework.4---56# Design QA78Binary pass/fail quality gates for UI code. Run against any component, page, or feature before shipping. Report ONLY failures — silence = quality.910**Fast scan** (<1s, requires `rg`, optional `sg`):11```bash12bash design-qa/scripts/design-scan.sh [dir] --fix # fix suggestions13bash design-qa/scripts/design-scan.sh [dir] --json # CI output14bash design-qa/scripts/design-scan.sh [dir] --no-ui # skip components/ui/15bash design-qa/scripts/design-scan.sh [dir] --critical-only16```1718**Deep review**: Read code → run gates in order → report failures with file:line and fix.1920---2122## Gate 1: Anti-Slop (Critical)2324ANY = immediate flag:2526| Check | Pass Condition |27|-------|---------------|28| No purple-blue gradients | Zero purple/blue/violet gradient combos |29| No gradient text | Zero `bg-clip-text text-transparent bg-gradient-*` on headings/metrics |30| No glassmorphism | Zero decorative `backdrop-blur` (functional overlay blur OK) |31| No hero metric template | No big-number-in-card × 3-4 identical row pattern |32| No identical card grid | Cards have varied content/layout |33| No glow effects | Zero colored `box-shadow` spread |34| No nested cards | Zero Card inside Card |35| Font is intentional | Not Inter/Roboto/Arial as default |3637---3839## Gate 2: Typography4041| Check | Verify |42|-------|--------|43| Project type scale only | Grep `text-[*px]`, `text-[*rem]`, `font-size:`. Only project-defined sizes. Exception: `text-[11px]` micro. |44| No letter-spacing mods | Grep `tracking-*`, `letter-spacing`. Zero unless in design spec. |45| Numeric data: tabular-nums | All `<td>/<th>` with numbers, prices, counts, dates use `tabular-nums`. |46| Headings: text-balance | All `<h1>`-`<h6>` use `text-balance` or `text-pretty`. |47| Line length controlled | Body text has `max-w-prose` or 45-75ch equivalent. |48| Max 3 font weights | Count distinct `font-*` weights. >3 = flag. |4950---5152## Gate 3: Color5354| Check | Verify |55|-------|--------|56| No hardcoded colors | Grep hex `#[0-9a-f]`, `rgb(`, `hsl(`, `oklch(` in JSX. All from tokens. |57| No pure black/white | `bg-black`, `bg-white`, `#000`, `#fff` on containers = flag. |58| No gray on colored bg | `text-gray-*` on non-neutral background = flag. Use bg shade. |59| Status colors semantic | Success=green, Error=red, Warning=amber, Info=blue. Inversion = flag. |60| Contrast ≥ 4.5:1 | OKLCH ΔL ≥ 0.4 body, ≥ 0.3 large text. |61| Max 2 accent colors | Count distinct accent/brand colors per view. >2 = flag. |6263---6465## Gate 4: Spacing6667| Check | Verify |68|-------|--------|69| No magic numbers | Grep `p-[*]`, `m-[*]`, `gap-[*]` with non-4px-multiple values. |70| Semantic hierarchy | Sections > groups > items spacing, consistent within page. |71| No triple responsive padding | `p-2 md:p-4 lg:p-6 xl:p-8` = flag. One value per context. |72| 4px grid | All spacing multiples of 4px (0.25rem). |7374---7576## Gate 5: Component Reuse7778| Check | Verify |79|-------|--------|80| Existing primitives used | Custom `<button>`, `<input>`, `<dialog>` when project has equivalents = flag. |81| No primitive mixing | Multiple UI lib imports (Radix + Base UI + Headless) in same file = flag. |82| CVA for variants | Inline ternary chains for 3+ style variants instead of CVA = flag. |83| data-slot attributes | Missing `data-slot` on component roots (if project convention) = flag. |8485---8687## Gate 6: Interaction States8889| Check | Verify |90|-------|--------|91| Hover on all buttons | Every `<Button>`/`<button>` has hover styles. |92| Focus visible everywhere | `:focus-visible` ring on all interactive elements. NEVER `outline: none` without replacement. |93| Disabled state real | `disabled:opacity-* + disabled:pointer-events-none`. Not just visual. |94| Loading states exist | Async actions show indicator. Buttons disable during submit. |95| Icon buttons: aria-label | Every icon-only button has `aria-label`. |96| Destructive → AlertDialog | Delete/discard/overwrite use AlertDialog, not Dialog/confirm. |97| Errors inline | Shown next to trigger, not only toast/banner. |98| Paste not blocked | No `onPaste={e => e.preventDefault()}`. |99100---101102## Gate 7: Accessibility103104| Check | Verify |105|-------|--------|106| Semantic HTML | `<button>` actions, `<a>` navigation, `<nav>/<main>/<header>`. No `<div onClick>`. |107| Heading hierarchy | h1→h2→h3 in order, no skips. One h1/page. |108| Alt text | Every `<img>` has `alt`. Decorative: `alt=""`. |109| Form labels | Every input has `<label>` (htmlFor or wrapping). Not just placeholder. |110| Keyboard nav | Logical tab order. No traps. All functionality keyboard-reachable. |111| ARIA live regions | Dynamic updates use `aria-live="polite"` or `role="status"`. |112| Color not sole indicator | Status paired with icon/text/pattern, never color alone. |113| Touch targets ≥ 44px | `min-h-11 min-w-11` or equivalent on mobile. |114| prefers-reduced-motion | All animations respect it. |115| h-dvh not h-screen | Grep `h-screen`. Replace with `h-dvh`. |116117---118119## Gate 8: Edge Case Hardening120121| Check | Verify |122|-------|--------|123| Empty states designed | Every list/table/grid has message + CTA. Not blank. |124| Long text handled | `truncate`, `line-clamp-*`, or `break-words` on names/titles. |125| Overflow prevented | Flex children: `min-w-0`. Grid children: `min-w-0 min-h-0`. |126| Numbers formatted | `Intl.NumberFormat` for large numbers. Not raw digits. |127| Dates formatted | `Intl.DateTimeFormat` or relative. Not raw ISO. |128| Skeletons match layout | Skeleton shapes mirror actual content structure. |129| Double-submit prevented | Submit buttons disable during async. |130| Error recovery | Every error state has retry or clear path forward. |131132---133134## Gate 9: Performance135136| Check | Verify |137|-------|--------|138| No layout animations | Grep `animate-*`/`transition-*` on width/height/top/left/margin/padding. Only transform+opacity. |139| No transition-all | Grep `transition-all`. Must specify: `transition-colors`, `transition-transform`, `transition-opacity`. |140| No permanent will-change | `will-change` only within animation scope. |141| No animated blur | `backdrop-blur` + `transition`/`animate` together = flag. |142| Images lazy loaded | Below-fold: `loading="lazy"`. |143| No layout shift | Async content has reserved space. |144| Lists virtualized | 100+ items use virtual scrolling. |145| Inputs debounced | Search/filter: 200-300ms debounce. |146| Cleanup on unmount | useEffect returns cleanup (cancel subs, abort fetch, remove listeners). |147| No render-logic useEffect | `useEffect` deriving values from props/state that could be computed during render = flag. |148| No console.log | Grep `console.log/warn/error` not in dev check. |149150---151152## Gate 10: Responsive153154| Check | Verify |155|-------|--------|156| No horizontal scroll | Mobile layout reflows to single column. |157| No hidden core features | `hidden md:block` on essential functionality = flag. |158| Logical CSS properties | `margin-inline-start/end` not `margin-left/right`. |159| No fixed text widths | `w-24`/`w-[200px]` on text containers = flag. |160| Zoom not disabled | No `user-scalable=no`/`maximum-scale=1`. WCAG violation. |161| Input method aware | `@media (pointer: coarse)` for touch targets. |162163---164165## Gate 11: Error Resilience166167| Check | Verify |168|-------|--------|169| API errors differentiated | 401→login, 403→permission, 404→not found, 429→rate limit, 500→generic. |170| Error boundaries | React boundaries around major sections. One crash ≠ full page down. |171| Optimistic rollback | Failed optimistic updates revert state + show error. |172173---174175## Report Format176177Output ONLY failures:178```179## QA Report: [Component/Page]180### Critical — [Gate]: [Check] — File:Line — Issue and fix181### High — [Gate]: [Check] — File:Line — Issue and fix182### Medium — [Gate]: [Check] — File:Line — Issue and fix183Summary: X critical, Y high, Z medium | Top pattern: [most repeated]184```185186**Severity**: Critical = a11y violations, anti-slop, broken states. High = missing loading, hardcoded colors, no empty states. Medium = typography, text-balance, debounce.187188---189190## Quick-Check (5 gates for PR review)1911921. No anti-slop (Gate 1)1932. No hardcoded colors (Gate 3)1943. Focus visible (Gate 6)1954. Empty states exist (Gate 8)1965. No layout animations (Gate 9)197198All pass → probably fine. Any fail → full gate sequence.