Prologue: The Day I Tried Using My Site Without a Mouse
One day my mouse battery died, and I tried using my own website with just the keyboard. I pressed Tab but couldn't see where the focus went. By the third click, I was just guessing. It took me a full minute to reach the "Login" button. That's when I realized: I couldn't use my own site.
Web accessibility (shortened to A11y - "a" + 11 letters + "y") wasn't some distant concern. It was my problem. And it wasn't just for people with disabilities. It was for people with broken arms, people using phones on the subway with one hand, my parents with aging eyes, and most importantly, my future self.
This isn't a "Web Accessibility Guide". It's my notes from learning through trial and error, from banging my head against code until things clicked.
The Struggle: div Buttons and Missing alt Text
When I first built websites, I made buttons like this:
<!-- Bad example: div button -->
<div class="btn" onclick="handleClick()">
Click me
</div>
It worked. It triggered the function when clicked. But it wasn't keyboard accessible. No matter how many times I pressed Tab, this "button" never received focus. When I turned on a screen reader, it just said "Click me" - no indication it was clickable at all.
And I added images like this:
<!-- Bad example: image without alt -->
<img src="/images/product.jpg">
When the image didn't load, there was just empty space. The screen reader said "image" - but what image? Nobody knew.
These two mistakes made my site inaccessible. When I realized it, I felt a chill down my spine.
The Aha Moment: Semantic HTML Was Everything
The solution was surprisingly simple. Semantic HTML - using meaningful tags. Buttons with <button>, images with alt, navigation with <nav>, main content with <main>. Following this principle solved 80% of the problems.
<!-- Good example: button tag -->
<button onclick="handleClick()">
Click me
</button>
After this change, pressing Tab automatically moved focus to the button. Enter triggered the click. The screen reader announced "Click me, button" - perfectly clear. Everything just worked, like magic.
<!-- Good example: alt attribute -->
<img src="/images/product.jpg" alt="Red sneakers product photo">
Adding alt meant that when the image didn't load, users saw "Red sneakers product photo" as text. Screen readers read the same thing. Users understood what the image was supposed to show.
I realized the essence of web accessibility is "telling the browser what things mean". A div is just a box, but a button tells the browser "this is clickable". That difference was everything.
Deep Dive 1: WCAG and Semantic Structure
WCAG (Web Content Accessibility Guidelines) is the W3C's web accessibility standard. I came to see it as an "accessibility checklist". There are four main principles:
- Perceivable: Users must be able to perceive the information. Alt for images, captions for videos.
- Operable: Users must be able to operate everything with a keyboard, not just a mouse.
- Understandable: Content and UI must be clear. Error messages should say "Password must be at least 8 characters", not just "Error".
- Robust: Must work with various assistive technologies (screen readers, etc).
Translating these principles into code gives you semantic HTML structure - dividing the page into meaningful sections.
<!-- Good example: semantic HTML structure -->
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>What is Web Accessibility?</h1>
<section>
<h2>Basic Concepts</h2>
<p>A web everyone can use...</p>
</section>
</article>
</main>
<footer>
<p>© 2025 My Site</p>
</footer>
With this structure, screen readers announced "header region", "main content", "footer". Users could jump directly to the section they wanted, like using a table of contents.
The bad example looked like this:
<!-- Bad example: all divs -->
<div class="header">
<div class="nav">
<div class="link">Home</div>
<div class="link">About</div>
</div>
</div>
<div class="content">
<div class="title">What is Web Accessibility?</div>
<div class="text">A web everyone can use...</div>
</div>
Screen readers just read "div, div, div...". No way to tell what was header and what was content. It looked pretty with CSS, but the meaning was lost.
I found this metaphor helpful: Semantic HTML is like putting signs in a building. "Restroom", "Emergency Exit", "Elevator". Without signs, you get lost in the building. Divs are blank walls without signs, while header/nav/main/footer are clear signs.
Deep Dive 2: ARIA - Explaining Complex UI
Sometimes semantic HTML isn't enough. Tabs, modals, dropdowns - these don't map to basic HTML tags. That's when you use ARIA (Accessible Rich Internet Applications).
ARIA has three parts:
- Roles: What this element is.
role="dialog",role="tab". - Properties: Element characteristics.
aria-label="Close button",aria-describedby="help-text". - States: Current state.
aria-expanded="false",aria-hidden="true".
Here's a modal example:
<!-- Good example: ARIA for modal -->
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">Login</h2>
<form>
<label for="email">Email</label>
<input type="email" id="email" aria-required="true">
<button type="submit">Submit</button>
</form>
<button aria-label="Close modal" onclick="closeModal()">
×
</button>
</div>
role="dialog" says "this is a modal". aria-modal="true" says "everything outside this modal is now inactive". aria-labelledby="modal-title" connects to the modal's title. Screen readers announce "Login dialog opened".
But ARIA is a double-edged sword. Used wrong, it makes accessibility worse. There's a saying: "No ARIA is better than Bad ARIA". For example, don't do this:
<!-- Bad example: unnecessary ARIA -->
<button role="button" aria-label="button">
Click me
</button>
<button> already has the button role. No need to add role="button". aria-label="button" is also redundant - the button already has "Click me" text. Overusing ARIA like this makes screen readers say "button, button, Click me, button" three times.
Here's how I summarized it: Only use ARIA when HTML isn't enough. If HTML solves it, you don't need ARIA.
Deep Dive 3: Keyboard Navigation and Focus Management
Imagine using a site without a mouse. Navigate with Tab, click with Enter or Space, close with Escape. That's keyboard navigation.
The most important part is focus management. Focus is the visual indicator saying "you are here". Usually a blue outline. Never remove it with CSS.
/* Bad example: removing focus outline */
button:focus {
outline: none;
}
This leaves keyboard users not knowing where they are. Do this instead:
/* Good example: custom focus style */
button:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Instead of removing the outline, customize it to look better. Users can clearly see where focus is.
Tab order is also important. When you press Tab, focus should move in a logical sequence. Usually follows HTML order. You can change it with tabindex, but be careful.
<!-- Bad example: confusing tabindex -->
<button tabindex="3">First</button>
<button tabindex="1">Second</button>
<button tabindex="2">Third</button>
This makes Tab go "Second → Third → First". Confusing. Better to not use tabindex and just structure your HTML logically.
However, tabindex="-1" is useful. It allows programmatic focus while preventing Tab key access. Used for moving focus into a modal when it opens.
// Good example: moving focus when opening modal
function openModal() {
const modal = document.getElementById('modal');
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
const firstInput = modal.querySelector('input');
firstInput.focus(); // Move focus to first input in modal
}
I also learned about skip links. Tabbing through long navigation every time is tedious. A hidden "Skip to main content" link at the top lets keyboard users jump straight to content.
<!-- Good example: skip link -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<!-- ... long navigation ... -->
<main id="main-content">
<h1>Main Content</h1>
</main>
/* Skip link hidden by default, visible on focus */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
First Tab shows the "Skip to main content" link. Press Enter and you jump straight to content. Super convenient.
Deep Dive 4: Color Contrast and Form Accessibility
Color contrast was important too. Light gray text on white background is hard to read for people with poor vision. WCAG AA standard is 4.5:1 (body text), AAA is 7:1.
I ran Chrome DevTools Lighthouse. Got an accessibility score of 70, with warnings about "insufficient text and background color contrast". Gray button text was the problem.
/* Bad example: insufficient contrast */
button {
background: #e0e0e0;
color: #999999; /* Contrast ratio 2.8:1 - fails */
}
Changed it to this:
/* Good example: sufficient contrast */
button {
background: #e0e0e0;
color: #333333; /* Contrast ratio 7.2:1 - AAA pass */
}
Darker text made it much easier to read. My eyes were less tired without me even realizing it.
Form accessibility was another lesson. Input fields must have connected <label> tags.
<!-- Bad example: input without label -->
<input type="text" placeholder="Enter your name">
With only placeholder, screen readers say "text input, blank". They don't know what input this is.
<!-- Good example: connected label -->
<label for="name">Name</label>
<input type="text" id="name" placeholder="Enter your name">
Connecting with for and id made screen readers say "Name, text input". Also, clicking the label focuses the input. Super convenient on mobile.
Error messages need to be clear too.
<!-- Bad example: unclear error message -->
<input type="email" id="email">
<span style="color: red;">Error</span>
Just showing "Error" doesn't tell you what's wrong. Change it to this:
<!-- Good example: clear error message -->
<label for="email">Email</label>
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
<span id="email-error" role="alert">
Invalid email format. Example: user@example.com
</span>
aria-invalid="true" announces "this field has an error". aria-describedby connects the error message. role="alert" makes screen readers read the error immediately. Users know exactly what to fix.
Real Application: Building a Dropdown Menu
Theory learned, time for practice. I built an accessible dropdown menu.
<!-- Accessible dropdown menu -->
<nav>
<ul role="menubar">
<li role="none">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="dropdown-menu"
id="menu-button"
>
Menu
</button>
<ul role="menu" id="dropdown-menu" hidden>
<li role="none">
<a href="/profile" role="menuitem">Profile</a>
</li>
<li role="none">
<a href="/settings" role="menuitem">Settings</a>
</li>
<li role="none">
<a href="/logout" role="menuitem">Logout</a>
</li>
</ul>
</li>
</ul>
</nav>
// Dropdown keyboard controls
const menuButton = document.getElementById('menu-button');
const dropdownMenu = document.getElementById('dropdown-menu');
menuButton.addEventListener('click', toggleMenu);
menuButton.addEventListener('keydown', handleKeydown);
function toggleMenu() {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true';
menuButton.setAttribute('aria-expanded', !isExpanded);
dropdownMenu.hidden = isExpanded;
if (!isExpanded) {
// Focus first item when opening menu
dropdownMenu.querySelector('[role="menuitem"]').focus();
}
}
function handleKeydown(e) {
if (e.key === 'Escape') {
toggleMenu();
menuButton.focus(); // Return focus to button when closing
}
}
With this implementation:
- Mouse click opens the menu.
- Tab to button and press Enter opens the menu.
- When menu opens, focus automatically moves to first item.
- Escape closes menu and returns focus to button.
- Screen reader announces states like "menu button, collapsed false", "menu opened".
For the first time, I felt "this is actually accessible".
Testing Tools: What's My Site's Score?
Testing is essential after building. I used three tools:
- Lighthouse (Chrome DevTools): Press F12, go to Lighthouse tab, check Accessibility. Automatically finds problems.
- axe DevTools: Chrome extension. Reports more detailed accessibility issues.
- Screen Reader Testing: Mac has VoiceOver (Cmd + F5), Windows has NVDA. You need to actually listen to find real problems.
First Lighthouse run gave me 68 points. Main issues:
- Images without alt
- Buttons without accessible names
- Insufficient color contrast
- Focus outline removed
Fixed them one by one and retested - got 94 points. Not perfect, but satisfying.
For screen reader testing, I closed my eyes. Turned on VoiceOver and tried using my site with keyboard only. Got to login in 30 seconds. Compared to the previous 1 minute, that's 50% improvement. All buttons were clearly announced, form inputs were manageable.
Closing: Small Considerations That Build the Web
When I first started learning web accessibility, I thought "this is too complicated". ARIA attributes had so much to memorize, WCAG guidelines were dry. But after actually doing it, I realized the core was simple: "Don't give others a site you can't use yourself".
Try using it without a mouse and the answers appear. Turn on a screen reader and the gaps show up. Run Lighthouse and you get a score. The tools are plentiful, the methods are clear. The problem was just doing it.
Now when I make a button, I use <button>. I add alt to images. I never remove focus outlines. I connect <label> to forms. These small habits accumulate to create "a web everyone can use", and I believe that.
I loved this final metaphor: Web accessibility is a ramp. Built for wheelchair users, but it helps parents with strollers, travelers with luggage, injured people - everyone. One alt tag, one <button> tag becomes someone's ramp. And someday it'll be my ramp. That's how I came to understand it.