frontend-a11y
Frontend A11y
Write as little code as possible. Use native HTML elements that are already accessible instead of adding ARIA attributes to generic elements.
Core Principles
- Trust the browser — Native elements have built-in accessibility
- Semantic over ARIA — Use the right element, not
roleattributes - Less is more — Every ARIA attribute you don't write is one less thing to break
- Native first — Use
<dialog>,<details>,<button>before reaching for JavaScript
HTML Guidelines
Use Native Elements
The browser already provides accessible elements. Use them.
<!-- Don't do this — too much code -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<!-- Do this — native button is already accessible -->
<button type="submit">Submit</button>
Don't Add Redundant Roles
Landmark elements already have implicit roles. Don't repeat them.
<!-- Don't do this -->
<header role="banner">...</header>
<nav role="navigation">...</nav>
<main role="main">...</main>
<footer role="contentinfo">...</footer>
<!-- Do nothing — the browser already handles this -->
<header>...</header>
<nav>...</nav>
<main>...</main>
<footer>...</footer>
Use Semantic Elements Over Divs
Replace generic containers with meaningful elements.
<!-- Don't do this -->
<div class="header">
<div class="nav">...</div>
</div>
<!-- Do this -->
<header>
<nav>...</nav>
</header>
Skip the Title Attribute
The title attribute is poorly supported. Only use it on <iframe>.
<!-- Don't do this -->
<button title="Submit form">Submit</button>
<!-- Only use title on iframe -->
<iframe src="..." title="Video player"></iframe>
Component Patterns
Use native elements that already have the behavior you need.
Accordion
Use native <details> and <summary>. No JavaScript needed.
<!-- Don't do this — too much code -->
<div class="accordion">
<button aria-expanded="false" aria-controls="panel-1">Section</button>
<div id="panel-1" hidden>Content</div>
</div>
<!-- Do this — zero JavaScript required -->
<details>
<summary>Section</summary>
<p>Content</p>
</details>
Modal Dialog
Use native <dialog> with showModal(). Focus trapping and Escape key handling are built-in.
<!-- Don't do this — requires focus trap JavaScript -->
<div role="dialog" aria-modal="true" aria-labelledby="title">
<h2 id="title">Title</h2>
<p>Content</p>
</div>
<!-- Do this — focus trap is automatic -->
<dialog id="my-dialog">
<h2>Title</h2>
<p>Content</p>
<button type="button" onclick="this.closest('dialog').close()">Close</button>
</dialog>
<button
type="button"
onclick="document.getElementById('my-dialog').showModal()"
>
Open dialog
</button>
The showModal() method automatically:
- Traps focus inside the dialog
- Closes on Escape key
- Adds the
::backdroppseudo-element - Marks content behind as inert
Navigation
Use <nav> with <button> and aria-expanded for dropdowns.
<nav>
<ul>
<li>
<button aria-expanded="false" aria-controls="submenu">Products</button>
<ul id="submenu" hidden>
<li><a href="/product-1">Product 1</a></li>
</ul>
</li>
</ul>
</nav>
Don't use role="menu", role="menuitem", or aria-haspopup for navigation.
Alert
A single role="alert" is all you need.
<div role="alert">Your changes have been saved.</div>
Other Patterns
When native elements aren't enough, follow these APG patterns:
- Feed Pattern for infinite scroll
- Combobox with Autocomplete for custom selects
- Switch Button Pattern for toggles
- Manual Tabs Pattern for tabs
- WAI Carousel Tutorial for carousels
CSS Guidelines
For general CSS best practices (units, logical properties, cascade layers, color spaces), use the appropriate CSS skill alongside this one — tiny-css for small or minimalist projects, more-css for anything larger. The patterns below are specific to accessibility.
Use ARIA Attributes as Styling Hooks
Don't create modifier classes when ARIA attributes already exist.
/* Don't do this — extra classes */
.accordion-header--collapsed {
}
.accordion-header--expanded {
}
/* Do this — style the ARIA state */
[aria-expanded="false"] {
}
[aria-expanded="true"] {
}
More examples:
[aria-current="page"] {
font-weight: bold;
}
[aria-disabled="true"] {
opacity: 0.6;
cursor: not-allowed;
}
[aria-selected="true"] {
background-color: highlight;
}
[aria-invalid="true"] {
border-color: red;
}
Don't Write All Caps in HTML
Use CSS instead so screen readers don't spell out letters.
<!-- Don't do this -->
<span>SUBMIT</span>
<!-- Do this -->
<span class="u-uppercase">Submit</span>
.u-uppercase {
text-transform: uppercase;
}
Meet Color Contrast Requirements
Text must meet WCAG 2.1 AA minimum contrast ratios — this is one of the most commonly failed accessibility checks. Always choose foreground/background color pairs that clear these thresholds:
- Normal text (below 18pt / 14pt bold): 4.5:1 minimum
- Large text (18pt+ / 14pt+ bold): 3:1 minimum
- UI components and focus indicators: 3:1 minimum against adjacent colors
Safe defaults that clear 4.5:1 without any calculation:
/* Dark text on light backgrounds */
body {
color: #404040;
background: #f4f4f4;
} /* ~9.43:1 */
.muted {
color: #636363;
background: #f4f4f4;
} /* ~5.46:1 */
/* Light text on dark backgrounds */
.dark {
color: #c9d4de;
background: #040014;
} /* ~13.75:1 */
/* Avoid these common low-contrast mistakes */
/* color: #767676 on white = exactly 4.5:1 — pass, but use sparingly */
/* color: #999 on white = 2.85:1 — FAIL */
/* color: #888 on #eee = 3.05:1 — FAIL for normal text */
Create Consistent Focus Outlines
Ensure all interactive elements have visible, high-contrast focus indicators.
*:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}
Handle Reduced Transparency
Only apply translucent or glassy effects when the user hasn't requested reduced transparency.
@media (prefers-reduced-transparency: no-preference) {
.glass-panel {
background: oklch(100% 0 0 / 0.8);
backdrop-filter: blur(1rem);
}
}
Respect Reduced Motion Preferences
Only animate elements when the user hasn't requested reduced motion.
@media (prefers-reduced-motion: no-preference) {
.animated-element {
transition: transform 0.3s ease;
}
}
Fade In Content Safely
Never use opacity: 0 alone to hide content before a fade-in animation. Screen readers ignore opacity — an element at opacity: 0 is still in the accessibility tree and will be announced before sighted users can see it.
A safe fade-in layers two protections:
- JS-ready gating so content stays visible by default if JavaScript fails to load
- IntersectionObserver so the animation triggers when the element enters the viewport — including when a screen reader's virtual cursor scrolls to it
<h1 class="fade-in">Welcome</h1>
@media (prefers-reduced-motion: no-preference) {
.js-ready .fade-in {
opacity: 0;
}
.fade-in.is-visible {
animation: fade-in 0.6s ease forwards;
}
}
@keyframes fade-in {
to {
opacity: 1;
}
}
document.documentElement.classList.add("js-ready");
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
}
}
});
for (const el of document.querySelectorAll(".fade-in")) {
observer.observe(el);
}
This works because:
- Without JavaScript, the
js-readyclass is never added — content stays fully visible and accessible - When a screen reader's virtual cursor reaches the element, the browser scrolls it into view, firing the IntersectionObserver and triggering the fade-in before the content is announced
- Users who prefer reduced motion never get the hidden state applied — content is visible immediately
Framework Guidelines
Don't use component libraries that introduce accessibility errors or wrap native browser behavior in unnecessary JavaScript. Libraries like Radix and Shadcn are common offenders — they ship <div>-based implementations for patterns the browser already handles natively.
Why Most Component Libraries Fail
- They re-implement focus trapping, keyboard navigation, and ARIA roles in JavaScript — behavior that
<dialog>,<details>, and<button>already provide for free - Accessibility bugs accumulate silently; library users rarely audit the underlying markup
- They create a dependency between your users and the library maintainer's willingness to fix a11y regressions
- More JavaScript, more surface area for bugs, harder to audit
Red Flags When Evaluating a Library
Reject any component library that:
- Uses
<div role="button">or<div role="dialog">instead of native elements - Implements its own focus trap when
<dialog>does it natively - Uses
aria-hiddenon visible, meaningful content - Requires JavaScript to expose keyboard navigation that HTML provides for free
- Has unresolved accessibility issues open in its issue tracker
If a Library Cannot Be Avoided
Audit it before shipping:
- Run axe DevTools on every component used
- Test with keyboard only — Tab, Enter, Escape, arrow keys
- Test with a screen reader — VoiceOver (macOS/iOS), NVDA or JAWS (Windows)
- Check the library's GitHub issues for open accessibility bugs
If it fails any of these, write the component natively instead.
References
Read these when you need more detail than the guidelines above:
- standards.md - Read when you need to cite WCAG 2.2 criteria, WAI-ARIA specs, or validation tools
- patterns.md - Read when implementing complex components like carousels, comboboxes, or feed patterns not covered above
- browser-support.md - Read when you need to verify browser support for a native HTML element or feature
More from mikemai2awesome/agent-skills
tiny-css
Write minimal, efficient CSS for small or minimalist projects by trusting the browser instead of fighting it. Only use this skill for personal sites, prototypes, simple landing pages, or projects intentionally kept lean — if the project has multiple developers, a component library, a design token system, or more than a handful of CSS files, a more comprehensive CSS approach is needed. If you're about to write a CSS reset, declare a base font-size on :root, set default colors on body, use px for spacing, or reach for physical margin/padding properties, this skill will stop you.
12tiny-a11y
Write minimal, accessible HTML, CSS, and JavaScript. Use when building web components, writing HTML markup, creating forms, or reviewing code for accessibility.
4frontend-conventions
Define and enforce consistent coding standards across HTML, CSS, and JavaScript. Always use this skill when naming a new class, variable, component, or file; setting up a new project's conventions; choosing a class prefix for a new CSS category; deciding on modifier API names (sizes, shades, hierarchy, breakpoints); or reviewing code for formatting and naming consistency. If you're about to invent a prefix, abbreviation, or modifier name without checking the conventions first, use this skill.
4