Synthesized from the design-system Claude Code plugin: WCAG 2.2 AA (W3C),
Don't Make Me Think (Krug), The Design of Everyday Things (Norman),
About Face (Cooper), Information Architecture for the Web (Rosenfeld &
Morville), Designing Interfaces (Tidwell), Grid Systems (Müller-Brockmann),
Refactoring UI (Wathan & Schoger), Inclusive Design Patterns (Pickering).
Format: each rule is PASS/FAIL, with a grep-able signal. Source in parens.
| # | Rule | FAIL signal |
|---|---|---|
| A1 | Text contrast ≥ 4.5:1 (normal), ≥ 3:1 (large/UI) (WCAG 1.4.3, 1.4.11) | Gray text lighter than ~#6b7280 on white; brand-color text on tinted bg |
| A2 | Every interactive element reachable via Tab; no positive tabindex (WCAG 2.1.1, 2.4.3) |
onClick on <div>/<span> without onKeyDown; tabindex="1+" |
| A3 | Focus indicator visible, ≥ 2px, ≥ 3:1 contrast (WCAG 2.4.7, 2.4.12) | outline: none / outline: 0 without :focus-visible replacement |
| A4 | Focused element never hidden by sticky header/footer (WCAG 2.4.11) | position: sticky/fixed over scrollable content without scroll-padding |
| A5 | Interactive targets ≥ 24×24 CSS px; ≥ 8px gap (WCAG 2.5.8) | Icon-only buttons with width/height < 24px; no min-height on hit areas |
| A6 | Native HTML over ARIA; never role that breaks native semantics (WAI-ARIA APG) |
<div role="button"> when <button> works; aria-hidden on focusable nodes |
| A7 | Every input has a programmatic label (<label for> or aria-label); placeholder ≠ label (WCAG 1.3.1, 3.3.2) |
<input placeholder="Email" /> with no <label> |
| A8 | Form errors associated via aria-describedby + aria-invalid="true" (WCAG 3.3.1) |
Error text near input but not wired with id/describedby |
| A9 | One <h1> per page; no skipped heading levels; landmarks present (<main>, <nav>, <header>) (WCAG 1.3.1, 2.4.6) |
Multiple <h1>; h2→h4; no <main> |
| A10 | <html lang="…"> set; foreign passages wrapped with lang (WCAG 3.1.1, 3.1.2) |
<html> missing lang |
| A11 | Pinch-zoom not disabled (WCAG 1.4.4) | <meta name="viewport" content="… user-scalable=no"> or maximum-scale=1 |
| A12 | Respects prefers-reduced-motion; no infinite animation without pause; no autoplay-with-sound (WCAG 2.3.3, 1.4.2) |
animation: spin … infinite with no @media (prefers-reduced-motion) block |
| A13 | Status/result announced via role="status" (aria-live="polite") or role="alert" (assertive only for errors) (WCAG 4.1.3) |
Toast appears with no live region |
| A14 | Color is never the only carrier of meaning (status, errors, required fields) (WCAG 1.4.1) | Red-only error states; required marked only by red asterisk color |
| A15 | Text-spacing override doesn't clip content: avoid fixed-height containers around text (WCAG 1.4.12) | height: 40px on a button containing dynamic text; overflow: hidden on prose |
| A16 | Skip-to-content link first focusable on long pages (WCAG 2.4.1) | No <a href="#main">Skip to content</a> |
| A17 | Allow paste in all text inputs incl. password; no character-by-character typing requirement (WCAG 3.3.8) | onPaste={(e) => e.preventDefault()} on password fields |
| A18 | Drag operations have a non-drag alternative (WCAG 2.5.7) | Drag-to-reorder with no menu/keyboard alternative |
| # | Rule | FAIL signal |
|---|---|---|
| B1 | Every page answers: Where am I? What is this page? What can I do? in < 5s (Krug) | Page lands without an <h1> matching the nav link the user clicked |
| B2 | Nav-link text exactly matches the destination page title (Krug — scent of information) | Nav says "Docs", page title says "Knowledge Base" |
| B3 | Labels are concrete & predictable; no marketing-speak ("Solutions", "Experiences", "Platform") (Krug, R&M) | Nav items that would fail the "what's behind this?" test |
| B4 | Link text self-sufficient out of context (no "click here", "learn more") (Krug, WCAG 2.4.4) | <a>Learn more</a> |
| B5 | Buttons named for the user's goal, not the system action: "Save changes" not "Submit" (Norman — mapping) | Generic "Submit" / "OK" / "Continue" |
| B6 | Affordances visible — controls look clickable: color or weight distinguishes them from prose (Norman — signifiers) | Buttons styled like text; links with no underline/color cue |
| B7 | Hover/press/focus states defined for every interactive element (Norman — feedback) | Component has :hover but no :focus-visible, or neither |
| B8 | Every async action gives feedback within 100ms; > 1s shows progress; > 10s shows %/ETA (Cooper, Nielsen) | await fetch(...) then navigate() with no setLoading(true) |
| B9 | Errors prevented by constrained input (number/date/select), not validated post-hoc (Norman — constraints) | <input type="text"> for quantity, then "must be 1–99" error |
| B10 | Forgiving formats: accept "1/15/26", "Jan 15 2026", "2026-01-15" (Tidwell) | Regex-strict date/phone/currency input |
| B11 | Reversible destructive actions use undo toast, not a confirm dialog (Cooper — excise) | if (confirm("Are you sure?")) for delete with server-side soft-delete available |
| B12 | Confirmation reserved for truly irreversible: send email, charge card, delete account (Cooper) | "Are you really sure?" double-confirms |
| B13 | One primary CTA per view; secondary actions visually muted; destructive separated (Refactoring UI, Tidwell) | Three same-weight buttons in a row |
| B14 | Smart defaults pre-filled (country, timezone, last-used value); never start with an empty form (Tidwell) | Required fields all blank on first load |
| B15 | Forms in a single column; related inputs grouped in <fieldset> (Tidwell — completion rates) |
Two- or three-column form bodies |
| B16 | Progressive disclosure for the 80/20: hide advanced behind <details>/"More options"; never hide the primary action (Cooper) |
All 12 settings visible at once; primary CTA inside "More options" |
| B17 | System state always visible: current page in nav (aria-current="page"), current mode/filter, unsaved-changes indicator (Norman) |
No active state on nav; no "unsaved changes" cue |
| # | Rule | FAIL signal |
|---|---|---|
| C1 | Any content reachable in ≤ 3 clicks from home (R&M — broad-and-shallow) | Leaf page 5+ clicks deep with no jump-link |
| C2 | Global nav has 5–9 top-level items (Miller's law, R&M) | Flat top nav with 12+ items |
| C3 | Logo top-left, links home; consistent across every page (Krug) | Logo non-clickable, or links elsewhere |
| C4 | Persistent global nav on every page (Krug) | Article pages drop the nav |
| C5 | Current-location indicator: aria-current="page" + visible highlight (R&M) |
Nav renders identically on every page |
| C6 | Breadcrumbs when hierarchy ≥ 3 levels deep; semantic <nav aria-label="Breadcrumb"><ol> (R&M) |
Deep section without "Home › … › Current" trail |
| C7 | Search present when content > 100 items; always visible (top), never icon-only (R&M) | Magnifying-glass icon collapses search behind a click |
| C8 | Don't mix organization schemes at the same level (R&M) | Top nav mixes audience ("For Devs") with topic ("Pricing") with task |
| C9 | No "mystery meat" — icons always paired with text labels (Krug) | Bottom tab bar with icons only |
| # | Rule | FAIL signal |
|---|---|---|
| D1 | Mobile-first CSS: base styles target small viewports; min-width media queries scale up (Refactoring UI) |
Base styles assume desktop; max-width media queries patch mobile |
| D2 | Breakpoints driven by content, not devices; common set: 640 / 768 / 1024 / 1280 (Tailwind/RUI) | @media (max-device-width: 414px) device sniffing |
| D3 | Layouts reflow column count, not shrink column width (Müller-Brockmann) | .card { width: 25% } becoming 80px on mobile |
| D4 | Cards/grids use repeat(auto-fit, minmax(280px, 1fr)) so density adapts (RUI) |
Fixed grid-template-columns: repeat(4, 1fr) at all sizes |
| D5 | Prose constrained to 45–75ch regardless of viewport (RUI, Krug) | <article> at full viewport width on large screens |
| D6 | Touch targets ≥ 44×44 on mobile (Apple HIG, WCAG AAA); ≥ 8px gap (WCAG 2.5.8) | Icon buttons 24×24 packed shoulder-to-shoulder on mobile |
| D7 | Primary nav: bottom tab bar or thumb-zone on mobile; top bar or sidebar on desktop (Tidwell, Krug Revisited) | Desktop top-nav crammed into mobile with no adaptation |
| D8 | No content hidden on mobile via display: none — reflow or summarize, don't drop (Krug) |
@media (max-width: 768px) { .secondary-info { display: none } } |
| D9 | Allow zoom; no maximum-scale=1 / user-scalable=no (WCAG 1.4.4) |
Restrictive viewport meta tag |
| D10 | Hover-only revealed content has a tap/focus equivalent (touch devices have no hover) (WCAG 1.4.13) | Tooltips/menus only visible on :hover |
| D11 | clamp() for fluid type/space between breakpoints; headings shrink less than body (RUI) |
Single font-size: 16px for body across all viewports |
| D12 | Tables: switch to stacked cards or horizontal scroll with visible affordance on narrow viewports | Data tables overflow viewport invisibly on mobile |
| D13 | Modals on mobile: full-screen sheets, not centered dialogs that get clipped | .modal { width: 600px } overflows mobile viewport |
| D14 | Forms: inputs full-width within their column; inputmode + correct type set (tel, email, numeric) |
<input type="text"> for phone — wrong mobile keyboard |
| D15 | Safe-area insets respected on notched devices (env(safe-area-inset-*)) |
Fixed bottom bar sits under the home indicator on iOS |
| D16 | Use dvh / svh not vh for full-height containers (mobile URL bar collapse) |
height: 100vh causes overflow when mobile chrome appears |
| D17 | Images use srcset + sizes or <picture> for responsive payload; aspect-ratio prevents CLS |
Single 2000px hero image served to all viewports |
| D18 | Keyboard appearance on mobile doesn't hide submit button: float CTA above keyboard or position: sticky |
Sticky-bottom button covered by iOS keyboard |
rg "outline\s*:\s*(none|0)" # A3, B7 — focus removed
rg "<div[^>]*onClick" # A2, A6 — non-button click handler
rg "tabindex\s*=\s*[\"']?[1-9]" # A2 — positive tabindex
rg "user-scalable\s*=\s*no|maximum-scale\s*=\s*1" # A11, D9
rg "<input(?![^>]*type=)" # D14 — untyped inputs
rg "display\s*:\s*none" --type css # D8 — mobile content hidden
rg "confirm\(" # B11 — confirm() dialogs
rg "100vh|min-height:\s*100vh" # D16 — vh trap
rg "@media[^{]*max-width" # D1 — desktop-first media queries
rg "placeholder=" -A2 -B1 | rg -v "<label" # A7 — placeholder-as-label (heuristic)- A1–A18 each worth 3 pts (54). Any FAIL on A2, A3, A6, A7, A9, A11 caps the score at 60 — those are legal-risk fails.
- B1–B17 each worth 1.5 pts (~25).
- C1–C9 each worth 1 pt (9).
- D1–D18 each worth ~0.7 pts (~12).
Score < 70 → not ready for production. Score < 50 → start over from layout/IA, not patches.
- W3C. Web Content Accessibility Guidelines (WCAG) 2.2. October 2023. https://www.w3.org/TR/WCAG22/
- Pickering, Heydon. Inclusive Design Patterns. Smashing Magazine, 2016.
- W3C. WAI-ARIA Authoring Practices 1.2. https://www.w3.org/WAI/ARIA/apg/
- Norman, Don. The Design of Everyday Things. Revised ed. Basic Books, 2013.
- Krug, Steve. Don't Make Me Think, Revisited. 3rd ed. New Riders, 2014.
- Cooper, Alan. About Face: The Essentials of Interaction Design. 4th ed. Wiley, 2014.
- Tidwell, Jenifer. Designing Interfaces. 3rd ed. O'Reilly, 2020.
- Rosenfeld, Louis & Morville, Peter. Information Architecture for the World Wide Web. 4th ed. O'Reilly, 2015.
- Müller-Brockmann, Josef. Grid Systems in Graphic Design. Niggli, 1981.
- Wathan, Adam & Schoger, Steve. Refactoring UI. 2018.
- Williams, Robin. The Non-Designer's Design Book. 5th ed. Peachpit, 2015.