Web Frontend Code Review Patterns
Stack-specific rules loaded by dh:code-reviewer when *.html, *.css, or *.jsx (browser-targeted) files are detected.
Accessibility
- Every interactive element (button, link, input, select) must have an accessible name — either visible text,
aria-label, oraria-labelledby - Focus must be managed when modals, dialogs, or dynamic panels open — trap focus inside and restore it on close
- Color contrast ratio must meet WCAG AA — 4.5:1 for normal text, 3:1 for large text (18px+ or 14px+ bold)
- Icon-only buttons without visible text must have
aria-labelor a visually-hidden text span - Form inputs must be associated with their label via
for/idpairing oraria-labelledby— proximity alone is not sufficient - Images conveying information must have descriptive
alttext; decorative images usealt="" - Keyboard navigation must work — focus order must follow visual order, no focus traps outside intentional modal patterns
XSS Prevention
element.innerHTML = userValueis a blocking finding — usetextContentfor plain textdangerouslySetInnerHTML(React) or equivalent without sanitization is a blocking finding- User-controlled values used in
eval(),Function(), orsetTimeout(string)are a blocking finding - URL values from user input used in
href,src, oractionattributes must be validated against an allowlist of schemes (blockjavascript:,data:)
Performance
- Layout thrash — reading layout properties (
offsetWidth,getBoundingClientRect) inside a loop that also writes styles — is a blocking finding - Images must have explicit
widthandheightattributes to prevent layout shift (CLS) - Images below the fold must use
loading="lazy"— eager loading of off-screen images delays critical rendering - Large JavaScript bundles loaded synchronously in
<head>withoutdeferorasyncare a blocking finding - Expensive computations inside render functions (React
render, Vue template expressions) without memoization are a blocking finding
CSS
!importantwithout an explanatory comment is a blocking finding- Design tokens (colors, spacing, typography) must use CSS custom properties (
--color-primary) — hardcoded hex values and px values are non-blocking but flagged - Magic
z-indexvalues without a documented z-index scale are a blocking finding — use named tokens (z-index: var(--z-modal)) position: fixedorposition: stickywithout overflow and scroll container awareness is flagged*selectors in component styles that may bleed into child components are a blocking finding
Forms
- Every
<input>,<select>, and<textarea>must have an associated<label>(either wrapping or viafor/id) autocompleteattributes must be set on inputs that collect personal data (name, email, address, payment) — required for WCAG 1.3.5- Validation error messages must be associated with the field via
aria-describedby— color alone is not sufficient to communicate errors - Form submission must not clear field values without user confirmation when validation fails
Event Listener Cleanup
- Event listeners added in component mount / setup must be removed in the corresponding unmount / cleanup
document.addEventListenerwithout a correspondingremoveEventListenerin the cleanup lifecycle is a blocking findingAbortControlleris preferred for fetch-and-cleanup patterns — pass the signal and abort on cleanup
Anti-Patterns
<!-- WRONG: no label, no accessible name -->
<input type="text" placeholder="Search..." />
<!-- RIGHT: associated label -->
<label for="search">Search</label>
<input type="text" id="search" autocomplete="off" />
<!-- WRONG: XSS vector -->
<div id="output"></div>
<script>
document.getElementById("output").innerHTML = userInput;
</script>
<!-- RIGHT: safe text insertion -->
<script>
document.getElementById("output").textContent = userInput;
</script>
/* WRONG: magic z-index */
.modal { z-index: 9999; }
/* RIGHT: design token */
.modal { z-index: var(--z-modal); }
/* WRONG: unexplained !important */
.button { color: red !important; }
/* RIGHT: explained override */
/* Overrides third-party widget styles that cannot be targeted more specifically */
.widget-container .button { color: red !important; }