Try using your web app with just your keyboard. No mouse, no trackpad. Tab through the page, hit Enter to click things, use arrow keys to navigate menus. I'll wait.
If you couldn't complete basic tasks — finding the navigation, submitting a form, closing a modal — then roughly 15% of the world's population is having the same struggle with your site every day. That's over a billion people with some form of disability.
Accessibility isn't a nice-to-have feature you bolt on at the end. It's a set of practices woven into how you write HTML, CSS, and JavaScript from the start. The good news? Most accessibility improvements are simple to implement. You just need to know what to look for.
Here's my practical checklist — the stuff that actually makes a difference.
Start With Semantic HTML (Seriously)
I know you've heard this before. But 80% of accessibility issues I encounter in code reviews stem from non-semantic HTML. Using a <div> with an onclick handler instead of a <button>. Building navigation with styled spans instead of <nav> and <a> tags.
Native HTML elements come with built-in accessibility features:
<button>— focusable, activatable via Enter and Space, announced as "button" by screen readers<a href>— focusable, activatable via Enter, announced as "link"<input type="checkbox">— togglable via Space, state announced automatically<select>— navigable via arrow keys, options announced correctly
When you use a <div> instead, you get none of that. You'd need to add tabindex, role, aria-pressed, keyboard event handlers, and focus styling — all of which the native element gives you for free.
<!-- Bad: reinventing the button -->
<div class="btn" onclick="handleClick()" tabindex="0"
role="button" aria-pressed="false">
Save
</div>
<!-- Good: just use a button -->
<button class="btn" onclick="handleClick()">
Save
</button>
Rule number one: use the right HTML element before reaching for ARIA. The W3C calls this the "first rule of ARIA" — if you can use a native element, do it.
ARIA: When Native HTML Isn't Enough
ARIA (Accessible Rich Internet Applications) attributes fill the gaps where HTML falls short. Custom components like tabs, accordions, modals, and autocomplete widgets don't have native HTML equivalents, so ARIA tells assistive technologies what they are and what state they're in.
The Essential ARIA Attributes
<!-- Labeling --> <button aria-label="Close dialog"> <svg>...</svg> <!-- Icon-only button needs a label --> </button> <!-- Describing relationships --> <input id="email" aria-describedby="email-hint"> <p id="email-hint">We'll never share your email.</p> <!-- State management --> <button aria-expanded="false" aria-controls="menu"> Menu </button> <nav id="menu" hidden>...</nav> <!-- Live regions for dynamic content --> <div aria-live="polite" aria-atomic="true"> <!-- Content updated via JS will be announced --> </div>
Let me break down the ones I use most often:
aria-label— provides a text label when there's no visible text (icon buttons, graphics)aria-labelledby— points to another element whose text serves as the labelaria-describedby— links to supplementary description text (form hints, error messages)aria-expanded— indicates whether a collapsible section is open or closedaria-hidden="true"— hides decorative elements from screen readersaria-live— makes dynamic content announce itself when updated
Common mistake: Don't use aria-label on elements that already have visible text. Screen readers will read the aria-label instead of the visible text, which is confusing if they don't match. Use it only for elements without visible text labels.
Keyboard Navigation: The Non-Negotiable
Every interactive element on your page must be reachable and operable with a keyboard. Period. This isn't just for screen reader users — it affects people with motor disabilities, power users who prefer keyboard shortcuts, and anyone with a broken mouse.
Focus Management Basics
/* Never do this */
*:focus {
outline: none;
}
/* Do this instead — custom focus styles */
:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: 4px;
}
The :focus-visible pseudo-class is your best friend. It only shows focus styles during keyboard navigation, not mouse clicks. This gives you the best of both worlds — visible focus indicators for keyboard users without the blue outline annoying mouse users.
Tab Order
The tab order should follow the visual order of your page. If your CSS layout reorders elements visually (using order, flex-direction: row-reverse, or absolute positioning), the tab order can become confusing because it still follows the DOM order.
<!-- Visual order: Search, Logo, Nav --> <!-- Tab order: Logo, Nav, Search (follows DOM) --> <header style="display:flex"> <div class="logo" style="order:1">Logo</div> <nav style="order:2">Nav links</nav> <input type="search" style="order:0"> </header> <!-- Better: match DOM order to visual order --> <header style="display:flex"> <input type="search"> <div class="logo">Logo</div> <nav>Nav links</nav> </header>
Focus Trapping for Modals
When a modal is open, Tab should cycle through only the modal's interactive elements — not escape to the page behind it. Here's a minimal focus trap:
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
first.focus();
}
Also: always handle the Escape key to close modals. Users expect it, and WCAG requires it.
Color Contrast: The Math Behind Readability
WCAG requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18px+ bold or 24px+ regular). These aren't arbitrary numbers — they're based on research into how various vision conditions affect readability.
That light gray text on a white background that looks "clean" to you? It probably fails contrast requirements. I've been guilty of this too — I once shipped an entire site with #999 text on #fff background. That's a 2.85:1 ratio. Fail.
/* Fails WCAG AA — 2.85:1 ratio */
.text-muted {
color: #999999;
background: #ffffff;
}
/* Passes WCAG AA — 4.64:1 ratio */
.text-muted {
color: #767676;
background: #ffffff;
}
/* Passes WCAG AAA — 7.0:1 ratio */
.text-muted {
color: #595959;
background: #ffffff;
}
Don't rely on color alone to convey information. Error states should use red AND an icon or text. Links should have underlines or another non-color indicator. Success messages should include a checkmark, not just green text. Roughly 8% of men have some form of color blindness.
Images and Media
Alt Text That's Actually Useful
Every <img> needs an alt attribute. But what you put in it matters more than just having it.
<!-- Bad: useless alt text --> <img src="chart.png" alt="image"> <img src="chart.png" alt="chart.png"> <!-- Bad: redundant with surrounding text --> <p>Our revenue grew 40% in Q3</p> <img src="chart.png" alt="Chart showing revenue grew 40% in Q3"> <!-- Good: describes what the image communicates --> <img src="chart.png" alt="Bar chart: Q3 revenue $2.1M, up 40% from Q2's $1.5M"> <!-- Decorative image: empty alt --> <img src="decorative-swirl.svg" alt="">
The rule: describe what the image communicates, not what it is. For decorative images that add no information, use alt="" (empty, not missing) so screen readers skip them entirely.
Forms: Where Accessibility Usually Falls Apart
Forms are where I see the most accessibility failures. Placeholder text used instead of labels. Error messages that aren't programmatically connected to inputs. Required fields with no indication.
<!-- Accessible form field -->
<div class="form-group">
<label for="user-email">
Email address <span aria-hidden="true">*</span>
</label>
<input
type="email"
id="user-email"
name="email"
required
aria-required="true"
aria-describedby="email-error"
aria-invalid="false"
>
<p id="email-error" class="error" role="alert" hidden>
Please enter a valid email address.
</p>
</div>
Key points:
- Always use
<label>with aforattribute matching the input'sid. Clicking the label should focus the input. - Don't use placeholder as a label. Placeholders disappear when typing, leaving users with no context.
- Connect error messages using
aria-describedbyand setaria-invalid="true"when validation fails. - Use
role="alert"on error messages so screen readers announce them immediately.
Testing Tools You Should Actually Use
Automated tools catch about 30-40% of accessibility issues. You still need manual testing. But they're a great starting point.
Browser Extensions
- axe DevTools (by Deque) — the gold standard. Scans your page and reports WCAG violations with clear fix suggestions. Free browser extension.
- WAVE — shows accessibility errors and warnings as visual overlays on your page. Great for quick visual audits.
- Lighthouse — built into Chrome DevTools (Audits tab). Includes an accessibility score and actionable recommendations.
Manual Testing Checklist
- Keyboard-only navigation: Tab through the entire page. Can you reach and operate everything?
- Screen reader test: Use VoiceOver (Mac), NVDA (Windows, free), or Narrator (Windows, built-in). Navigate your page and listen to what's announced.
- Zoom test: Zoom to 200%. Does the layout break? Is content still readable?
- Color contrast check: Run all text through a contrast checker (WebAIM's is free and simple).
- Reduced motion: Turn on "reduce motion" in your OS settings. Do animations respect
prefers-reduced-motion?
// Quick check: log all elements with missing alt text
document.querySelectorAll('img:not([alt])').forEach(img => {
console.warn('Missing alt text:', img.src);
});
// Check for clickable divs that should be buttons
document.querySelectorAll('div[onclick], span[onclick]').forEach(el => {
console.warn('Non-semantic clickable element:', el);
});
Visualize Your Accessible HTML
Built accessible components? Convert your HTML to crisp images for documentation, training materials, or presentations.
Try HTML to PNG Free →Quick Wins You Can Ship Today
If you're looking at a large existing codebase and feeling overwhelmed, start with these high-impact, low-effort improvements:
- Add
langattribute to<html>. Screen readers use this to determine pronunciation. Takes 5 seconds. - Add a skip-to-content link. One
<a>tag that jumps past navigation to<main>. - Fix your heading hierarchy. One
<h1>, then<h2>s, then<h3>s. No skipping levels. - Add
altto all images. Emptyalt=""for decorative ones. - Remove
outline: nonefrom focus styles. Replace with:focus-visiblestyles. - Add
aria-labelto icon-only buttons. Every button needs an accessible name.
These six changes will fix the majority of common accessibility issues on most sites. You can do all of them in under an hour.
Accessibility Is a Spectrum, Not a Checkbox
You'll never make a site "100% accessible" — that's like saying code is "100% bug-free." But you can make deliberate, continuous improvements that meaningfully impact real people's ability to use your product.
Start with semantic HTML. Test with your keyboard. Run axe DevTools. Fix what it finds. Then try a screen reader and see what surprises you. I guarantee you'll find something you didn't expect.
The best part? Most accessibility improvements also make your site better for everyone. Keyboard navigation benefits power users. Color contrast helps in bright sunlight. Proper form labels help anyone who's ever wondered "what does this field want?" Good accessibility is just good UX.