Skip to content

Instantly share code, notes, and snippets.

@kevinsperrine
Created May 13, 2026 22:56
Show Gist options
  • Select an option

  • Save kevinsperrine/c16f22ac98b5dc865f38fc68c1f26643 to your computer and use it in GitHub Desktop.

Select an option

Save kevinsperrine/c16f22ac98b5dc865f38fc68c1f26643 to your computer and use it in GitHub Desktop.
Frontend code review rule set: WCAG 2.2 AA, Krug/Norman cognition, responsive/mobile breakpoints. Synthesized from the design-system Claude Code plugin.

Frontend Review Rule Set — A11y, Cognition, Responsive

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.


A. WCAG 2.2 AA — Accessibility (must pass)

# 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>; h2h4; 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

B. "Don't Make Me Think" — Cognition & Labels

# 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

C. Information Architecture — Three-Click & Findability

# 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

D. Responsive & Mobile Breakpoints

# 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

E. Quick Triage — Top Greps for Code Review

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)

F. Heuristic Score — Sum 0–100

  • 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.


Sources

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment