Why Accessibility Matters
The Web Content Accessibility Guidelines (WCAG) define four principles for accessible content, summarised as POUR:
- Perceivable β information must be presentable in ways all users can perceive (e.g., alt text for images).
- Operable β interface components must be operable by keyboard, not just mouse.
- Understandable β content and UI must be understandable.
- Robust β content must be interpreted reliably by assistive technologies.
WCAG 2.1 has three conformance levels: A (minimum), AA (standard legal target), and AAA (enhanced). Most accessibility legislation requires AA compliance.
Semantic HTML is the Foundation
The single most impactful accessibility action is to use the right HTML element for the right job. Semantic elements carry built-in accessibility information that browsers expose to screen readers automatically β no extra work required.
<!-- Bad: div soup β screen readers have no context -->
<div class="header">
<div class="nav">
<div class="nav-item" onclick="go('/')">Home</div>
</div>
</div>
<!-- Good: semantic HTML is accessible by default -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
A screen reader announces a <button> as "button, activatable". A plain <div> with an onclick handler announces nothing useful. Use native elements whenever possible β <button>, <a>, <input>, <select> are all keyboard-focusable and announced correctly out of the box.
Images: Descriptive alt Text
Every <img> element must have an alt attribute. Screen readers read the alt text aloud. Search engines also use it for image indexing.
<!-- Informative image: describe what the image shows and why it matters -->
<img src="html-structure.png" alt="Diagram showing the HTML document tree with html, head, and body elements">
<!-- Functional image (linked image): describe the destination/action, not the image -->
<a href="/">
<img src="logo.png" alt="ylearner β Go to homepage">
</a>
<!-- Decorative image: empty alt (screen readers skip it entirely) -->
<img src="decorative-wave.svg" alt="">
<!-- Bad: filename as alt (useless to screen reader users) -->
<img src="img_001.jpg" alt="img_001.jpg">
<!-- Bad: "image of" / "photo of" prefix (redundant β screen readers announce it is an image) -->
<img src="chart.png" alt="Image of a bar chart">
<!-- Good: describe content meaningfully -->
<img src="chart.png" alt="Bar chart showing 40% increase in web traffic from January to June 2026">
alt attribute is a WCAG failure. An empty alt="" is intentional β it tells screen readers "skip this image." Never omit the attribute entirely.
Forms: Labels, Fieldsets, and Error Messages
Forms are the area where accessibility failures are most common. Every form control must have a visible, programmatically associated label.
<!-- Method 1: label with for + matching input id (preferred) -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" required autocomplete="email">
<!-- Method 2: wrapping label -->
<label>
Username
<input type="text" name="username" required>
</label>
<!-- Bad: placeholder is NOT a label -->
<input type="email" placeholder="Email address">
<!-- Grouping related inputs with fieldset and legend -->
<fieldset>
<legend>Preferred contact method</legend>
<label><input type="radio" name="contact" value="email"> Email</label>
<label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>
<!-- Accessible error message linked to input -->
<label for="password">Password</label>
<input type="password" id="password" name="password"
aria-describedby="password-error" aria-invalid="true">
<span id="password-error" class="error" role="alert">
Password must be at least 8 characters.
</span>
Keyboard Navigation: tabindex, Focus Styles, Skip Links
Many users β including those with motor disabilities and power users β navigate entirely by keyboard. Tab moves forward through focusable elements; Shift+Tab moves backward; Enter/Space activates buttons and links.
<!-- Skip link: allows keyboard users to jump past repeated navigation -->
<!-- Place as the very first element in body -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- tabindex="0": adds a non-interactive element to the tab order -->
<div role="button" tabindex="0" onclick="doAction()"
onkeydown="if(event.key==='Enter'||event.key===' ')doAction()">
Click me
</div>
<!-- Note: use a real <button> instead when possible! -->
<!-- tabindex="-1": removes element from tab order but allows programmatic focus -->
<div id="modal" tabindex="-1">β¦modal contentβ¦</div>
<!-- document.getElementById('modal').focus() works, Tab key skips it -->
<!-- tabindex values greater than 0 create unpredictable tab order β AVOID -->
<!-- Bad: -->
<input tabindex="3">
<input tabindex="1">
<input tabindex="2">
outline: none or outline: 0 without a replacement focus style is a WCAG 2.1 Level AA failure (Success Criterion 2.4.7). Always provide a visible focus indicator.
ARIA Attributes
ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that supplement the accessibility information of elements when native semantics are insufficient. The first rule of ARIA: don't use ARIA if a native HTML element provides the semantics you need.
<!-- aria-label: provides an accessible name when no visible text label exists -->
<button aria-label="Close dialog">β</button>
<input type="search" aria-label="Search tutorials">
<!-- aria-describedby: links to text that provides additional description -->
<input type="text" id="zip" aria-describedby="zip-hint">
<p id="zip-hint">Enter your 5-digit US ZIP code.</p>
<!-- aria-hidden: hides element from accessibility tree (e.g. decorative icons) -->
<span aria-hidden="true">π </span>
<span>Home</span>
<!-- aria-expanded: communicates open/closed state of collapsible content -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden><li><a href="/">Home</a></li></ul>
<!-- aria-live: announces dynamic content changes to screen readers -->
<div aria-live="polite" aria-atomic="true">
<!-- Updated content will be announced when changed -->
Form submitted successfully.
</div>
<!-- aria-current: marks the current item in a set (e.g. current nav link) -->
<nav>
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
</nav>
ARIA Landmark Roles
Landmark roles allow screen reader users to jump directly to major page regions. HTML5 semantic elements map to landmark roles automatically β use the native elements and you get the landmarks for free.
| Landmark Role | Native HTML Element | Purpose |
|---|---|---|
banner | <header> (top-level) | Site header with logo and primary navigation |
navigation | <nav> | Group of navigational links |
main | <main> | Primary page content (one per page) |
complementary | <aside> | Content that supports but is not essential to main |
contentinfo | <footer> (top-level) | Footer with copyright, legal links |
search | <search> or role="search" | Search functionality |
form | <form> with aria-label | Labelled form region |
region | <section> with aria-label | Labelled section of significant content |
<!-- When you have multiple nav elements, label them for distinction -->
<nav aria-label="Main navigation">β¦</nav>
<nav aria-label="Breadcrumb">β¦</nav>
<nav aria-label="Pagination">β¦</nav>
<!-- Explicit landmark role when element cannot be semantic -->
<div role="main">β¦</div> <!-- Only if <main> is not usable -->
Color Contrast
Approximately 8% of men and 0.5% of women have some form of color vision deficiency. WCAG 2.1 defines minimum contrast ratios for text against its background:
| Text Type | Level AA (minimum) | Level AAA (enhanced) |
|---|---|---|
| Normal text (under 18pt / 14pt bold) | 4.5:1 | 7:1 |
| Large text (18pt+ / 14pt+ bold) | 3:1 | 4.5:1 |
| UI components and graphics | 3:1 | β |
Use the WebAIM Contrast Checker or browser DevTools' accessibility panel to verify contrast. Never convey information using color alone β also use shape, pattern, or text labels.
Testing Accessibility
Automated tools catch about 30β40% of accessibility issues. Manual testing is essential:
- Browser DevTools Accessibility panel β Chrome and Firefox both have built-in accessibility trees showing element roles, names, and states.
- axe DevTools (free browser extension) β scans the page and reports WCAG violations with links to how-to-fix guides.
- Lighthouse β built into Chrome DevTools, produces an accessibility score with actionable items.
- Keyboard-only testing β unplug your mouse and navigate your entire page using only Tab, Shift+Tab, Enter, Space, and arrow keys.
- Screen reader testing β NVDA (Windows, free), JAWS (Windows, paid), VoiceOver (macOS/iOS, built in), TalkBack (Android, built in).
π Summary
- Semantic HTML (
<button>,<nav>,<main>, etc.) provides accessibility for free β use it instead of div soup. - Every
<img>needs analtattribute: descriptive for informative images, empty (alt="") for decorative ones. - Every form control needs a programmatically associated
<label>; use<fieldset>and<legend>for groups of related inputs. - Add a skip link as the first element in
<body>so keyboard users can bypass repeated navigation. - Never remove focus styles without providing a visible replacement.
- ARIA attributes supplement native semantics β do not use ARIA where a native element already provides the role.
- Aim for WCAG 2.1 Level AA compliance: 4.5:1 contrast for normal text, 3:1 for large text and UI components.
FAQ
Is ARIA necessary if I use semantic HTML?
Often not. Semantic HTML elements carry their own ARIA roles implicitly. For example, <button> has an implicit role="button", <nav> has role="navigation". You only need explicit ARIA when you are building a custom interactive widget (like a combobox, date picker, or carousel) that has no semantic HTML equivalent, or when you need to communicate dynamic state changes (like aria-expanded or aria-live).
What is the difference between aria-label and aria-labelledby?
aria-label provides an accessible name as a direct string value: aria-label="Close". aria-labelledby references the ID of an existing element whose text content becomes the accessible name: aria-labelledby="dialog-title". Use aria-labelledby when the label text is already visible on screen (avoiding duplication), and aria-label when there is no visible text to reference (icon-only buttons, etc.).
Do placeholder attributes count as labels?
No. Placeholder text fails as a label for several reasons: it disappears when the user starts typing (making it impossible to check what was required), it often has insufficient color contrast, and older screen readers do not consistently announce placeholders as labels. Always use a visible <label> element. Placeholder text can supplement a label but must never replace it.