Beginner's Deep Dive: Web Accessibility (A11y)
A hands-on overview of accessibility fundamentals you can apply today. Uses native HTML first, minimal ARIA, keyboard-first thinking.
What accessibility is (and why it matters)
Accessibility ensures everyone can use your site—keyboard users, screen-reader users, people who zoom, prefer reduced motion, or have color-vision differences. It's good ethics, good UX, and helps SEO through clearer semantics.
- Perceivable — alternatives like text, captions.
- Operable — keyboard accessible, no traps.
- Understandable — clear labels, instructions, errors.
- Robust — semantic HTML that works with assistive tech.
The mindset (before code)
- Use native HTML first; add ARIA only if native elements can't express it.
- Make sure everything is usable by keyboard.
- Keep focus visible and predictable.
- Provide text alternatives for non-text content.
- Don't rely on color alone to convey meaning.
1) Semantic HTML: the biggest win
<body>
<header>…site header…</header>
<nav aria-label="Primary">…links…</nav>
<main id="content">…unique content…</main>
<aside>…related info…</aside>
<footer>…copyright…</footer>
</body>
Headings: one <h1> per page, then nest h2, h3 in order—do't skip levels for style.
<h1>Enchanted Yule Cards</h1>
<h2>Top Sellers</h2>
<h3>Krampus & Yule Cat</h3>
Links vs Buttons: links go to URLs; buttons do actions.
<a href="/cards/yule-cat" class="card">View details</a>
<button type="button" id="addToCart">Add to basket</button>
2) Images & icons (alt text that sells)
<img src="yule-cat-card-front.jpg"
alt="Funny Yule card: Krampus dangling from the Yule Cat's mouth">
<img src="snowflakes.svg" alt="" aria-hidden="true">
<button type="button" aria-label="Add to basket">
<svg aria-hidden="true" focusable="false">…</svg>
</button>
3) Color & contrast
Aim for contrast ratio ≥ 4.5:1 for body text (3:1 for large text). Don't communicate status by color alone.
<p><strong class="status-dot" aria-hidden="true">●</strong>
<span class="sr-only">Status:</span> In stock</p>
.sr-only{
position:absolute; width:1px; height:1px; padding:0; margin:-1px;
overflow:hidden; clip:rect(0,0,1px,1px); white-space:nowrap; border:0;
}
4) Keyboard accessibility & focus
:focus { outline: 3px solid currentColor; outline-offset: 2px; }
:focus-visible { outline: 3px solid; }
Skip link to jump to main content:
<a class="skip-link" href="#content">Skip to content</a>
5) Forms that make sense (labels, hints, errors)
<form novalidate>
<div>
<label for="email">Email</label>
<input id="email" name="email" type="email"
autocomplete="email" aria-describedby="email-hint email-err">
<div id="email-hint" class="hint">We'll send updates here.</div>
<div id="email-err" class="error" role="alert">Enter a valid email.</div>
</div>
<fieldset>
<legend>Card finish</legend>
<label><input type="radio" name="finish" value="matte"> Matte</label>
<label><input type="radio" name="finish" value="gloss"> Gloss</label>
</fieldset>
<button type="submit">Checkout</button>
</form>
6) ARIA: when (and how) to use it
- Use
role="alert"for immediate, important messages. - Use
aria-expanded,aria-controlsfor disclosure widgets. - Use
aria-live="polite"for quieter updates.
<p id="cart-updates" aria-live="polite"></p>
<button id="add">Add to basket</button>
<script>
document.getElementById('add').addEventListener('click', () => {
document.getElementById('cart-updates').textContent =
'Added “Yule Cat Card” to basket.';
});
</script>
7) Disclosures/accordions (FAQ, product details)
<button aria-expanded="false" aria-controls="a1" id="q1">
What paper stock is used?
</button>
<div id="a1" role="region" aria-labelledby="q1" hidden>
300gsm card, smooth finish.
</div>
<script>
const btn = document.getElementById('q1');
btn.addEventListener('click', () => {
const expanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!expanded));
document.getElementById('a1').hidden = expanded;
});
</script>
8) Multimedia (video, motion)
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; animation-iteration-count: 1 !important;
transition-duration: .001ms !important; }
}
<video controls>
<source src="product-demo.mp4" type="video/mp4">
<track kind="captions" src="product-demo.en.vtt" srclang="en" label="English">
Your browser does not support the video tag.
</video>
9) Tables (specs, sizes)
<table>
<caption>Card sizes and envelopes</caption>
<thead>
<tr><th scope="col">Size</th><th scope="col">Inches</th><th scope="col">Envelope</th></tr>
</thead>
<tbody>
<tr><th scope="row">Standard</th><td>5" x 7"</td><td>White</td></tr>
</tbody>
</table>
10) Page essentials
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Funny Pagan Yule Cards | Webliminal Arts</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
11) Single-page apps (React/Vue)
// After navigation:
document.querySelector('h1')?.focus(); // ensure h1 is focusable via tabindex="-1"
<h1 tabindex="-1">Checkout</h1>
12) Quick checklist
- Keyboard access: Tab/Shift+Tab/Enter/Space/Escape/Arrows work as expected.
- Visible focus for all interactive elements.
- One
h1, logical heading order. - Alt text on meaningful images;
alt=""on decorative. - Labels on form fields; clear hints and error text.
- Color contrast ≥ 4.5:1 for body text.
- Skip link and correct landmarks.
- No “div-buttons”. Use
<button>/<a>. - Respects
prefers-reduced-motion. langattribute and descriptive<title>.
13) Testing (quick and free)
- Keyboard only: Try to complete a key task without a mouse.
- Screen readers: NVDA (Windows), VoiceOver (macOS).
- Automated checks: Chrome Lighthouse (Accessibility), axe DevTools.
- Contrast: verify text and icons with a contrast checker.
14) Starter utilities
<div aria-live="polite" class="sr-only" id="global-updates"></div>
<script>
function announce(msg){
const live = document.getElementById('global-updates');
live.textContent = ''; // force change for some SRs
setTimeout(() => live.textContent = msg, 50);
}
</script>
<div role="alert" class="toast" id="toast" hidden>Added to basket</div>
<script>
function showToast(text){
const t = document.getElementById('toast');
t.textContent = text;
t.hidden = false;
setTimeout(() => t.hidden = true, 3000);
}
</script>
15) Common anti-patterns
- Using placeholder text instead of a real
<label>. - Click handlers on non-interactive elements (e.g.,
<div>). - Removing focus outlines without replacement.
- Icon-only buttons with no accessible name.
- Auto-advancing carousels with no pause/controls.
- Modals that don't trap focus or close with Escape.
