tiny-a11y
Tiny 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
References
- Use WCAG 2.2 Understanding for accessibility guidance
- Use WAI-ARIA 1.2 for ARIA attributes
- Use APG Gherkin for component test cases
- Use Design Tokens for design systems terminology
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
Use OKLCH for Colors
OKLCH provides a wider color gamut and perceptually uniform lightness.
:root {
--color-primary: oklch(50% 0.2 260);
--color-surface: oklch(98% 0 0);
}
Use Relative Units
Use rem, em, %, vw, vh instead of px, except for borders.
/* Don't do this */
.card {
padding: 16px;
font-size: 14px;
}
/* Do this */
.card {
padding: 1rem;
font-size: 0.875rem;
}
Use Logical Properties
Support all languages and writing directions.
/* Don't do this */
.card {
margin-left: 1rem;
padding-top: 2rem;
width: 20rem;
}
/* Do this */
.card {
margin-inline-start: 1rem;
padding-block-start: 2rem;
inline-size: 20rem;
}
Use Cascade Layers
Organize CSS in this order: @layer config, resets, components, utilities.
@layer config, resets, components, utilities;
@layer config {
:root {
--color-primary: oklch(50% 0.2 260);
}
}
@layer resets {
/* CSS resets */
}
@layer components {
.c-button {
/* component styles */
}
}
@layer utilities {
.u-visually-hidden {
/* utility styles */
}
}
Use Class Prefixes
c-for component classesu-for utility classesjs-for JavaScript selectors
<div class="c-card js-accordion">...</div>
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;
}
Use Focus-Visible
Only show focus rings when needed.
/* Don't do this — shows ring on click */
button:focus {
outline: 2px solid;
}
/* Do this — only shows ring for keyboard users */
button:focus-visible {
outline: 2px solid;
outline-offset: 2px;
}
Respect Motion Preferences
Only animate when the user allows it.
@media (prefers-reduced-motion: no-preference) {
.animated {
transition: transform 0.3s ease;
}
}
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;
}
JavaScript Guidelines
- Use vanilla JavaScript only
- Don't use component libraries (Radix, Shadcn)
- Don't use utility frameworks (Tailwind CSS)
Quick Reference
| Instead of | Use |
|---|---|
<div role="button"> |
<button> |
<div role="dialog"> |
<dialog> |
<div role="navigation"> |
<nav> |
<div role="banner"> |
<header> |
<div role="main"> |
<main> |
<div role="contentinfo"> |
<footer> |
| Custom accordion JS | <details> + <summary> |
| Focus trap library | Native <dialog> |
.is-expanded class |
[aria-expanded="true"] |