Enforce accessibility best practices when building or reviewing any HTML, CSS, or frontend code. Use this skill whenever creating web pages, forms, navigation, modals, interactive components, or any UI that a user will interact with. Trigger on any frontend HTML/CSS work.
Accessibility is not a checklist item to bolt on at the end — it's a design constraint that improves the experience for everyone, including keyboard users, screen reader users, people on slow connections, and people on mobile devices.
The legal risk is also real: WCAG non-compliance has resulted in lawsuits. Build it right the first time.
Before reaching for ARIA, ask: is there a native HTML element that does this already?
| Instead of | Use |
|---|---|
<div onclick="..."> | <button> |
<div role="navigation"> | <nav> |
<div role="main"> | <main> |
<div role="header"> | <header> |
<span> as a heading | – |
<h1><h6><div> for a list | <ul> / <ol> |
| Custom checkbox | <input type="checkbox"> styled with CSS |
Native elements come with keyboard interaction, focus management, and screen reader announcements for free. Custom ARIA is a fallback for when native won't work, not a first choice.
Every page must have:
<h1> per page — it's the primary heading, not a style choiceh1 → h2 → h3, never skip levels (h1 → h3 is wrong)<main> landmark wrapping the primary content<nav> for navigation regions (can have multiple — use aria-label to distinguish)lang attribute on <html>: <html lang="en"><title> tag — not just "Home" or the site nameRecommended way to declare accessible colors with CSS variables:
:root {
--color-text: #1a1a1a; /* 14.7:1 on white */
--color-surface: #ffffff;
--color-accent: #0055cc; /* 6.2:1 on white */
--color-muted: #5c5c5c; /* 7.0:1 on white */
}
Every interactive element must be:
outline: none without a custom focus style/* Good: custom focus style that's visible */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
border-radius: 2px;
}
/* Bad: removes focus entirely */
* { outline: none; }
Skip links are required on pages with navigation before the main content:
<a href="#main-content" class="skip-link">Skip to main content</a>
<img> elements need an alt attribute (empty string alt="" is correct for decorative images)<title> and/or aria-label when they convey meaningaria-hidden="true"<!-- Informative image -->
<img src="diagram.png" alt="Architecture diagram showing the three-tier service layout">
<!-- Decorative image -->
<img src="squiggle.png" alt="">
<!-- Icon button -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<input> needs a <label> — using placeholder as a label is not accessiblefor/id pairing or wrap input inside the label<fieldset> and <legend> (especially radio/checkbox groups)aria-required="true" and indicate it visually (not just with red color)aria-describedby<label for="email">Email address <span aria-hidden="true">*</span></label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-describedby="email-error"
>
<span id="email-error" role="alert" class="error-message">
Please enter a valid email address
</span>
role="dialog" + aria-modal="true" + aria-labelledbyaria-expanded on the trigger buttonaria-haspopup="true" if it's a menuaria-live="polite" for async content updatesaria-busy="true" on containers that are loadingAlways respect the user's motion preference:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
This is especially important for scroll animations, parallax, and auto-playing content.
Before marking any component complete:
<main> landmark?prefers-reduced-motion respected?