Accessibility, often called A11y, means building websites that can be used by as many people as possible, including people who use:
Accessibility isn't a separate feature you add at the end.
Good HTML is one of the foundations of accessibility.
You may see:
A11yThis is shorthand for:
AccessibilityWhy?
A + 11 letters + ySo:
Accessibility
↓
A11ySimilarly:
Internationalization → i18n
Localization → l10nImagine a website containing:
Logo
Navigation
Search
Products
CheckoutA mouse user may easily click everything.
But imagine someone who navigates entirely with a keyboard:
Tab
↓
Tab
↓
Tab
↓
EnterOr someone using a screen reader:
"Heading level 1, Online Store"
"Navigation"
"Link, Home"
"Link, Products"
"Main"
"Button, Add to cart"For this to work properly, your HTML needs to communicate the meaning and relationships of the content.
A common misconception is:
Accessibility = screen readers.
That's only one part.
Accessibility includes:
Accessibility
│
├── Vision
│ ├── Screen readers
│ ├── Low vision
│ ├── Magnification
│ └── Color perception
│
├── Hearing
│ └── Captions/transcripts
│
├── Motor
│ ├── Keyboard
│ ├── Switch devices
│ └── Voice control
│
└── Cognitive
├── Clear language
├── Predictable navigation
└── Understandable interfacesBefore learning ARIA, remember:
Use native HTML elements whenever possible.
For example:
Correct:
<button type="button">
Save
</button>Instead of creating:
<div onclick="save()">
Save
</div>Why?
A native button already provides browser accessibility behavior.
Consider:
<button>Submit</button>The browser already knows:
This is a buttonIt can generally:
Now compare:
<div>Submit</div>The browser sees:
Generic containerIt doesn't automatically know that it's supposed to behave like a button.
You already learned semantic HTML.
Examples:
<header>
<nav>
<main>
<section>
<article>
<aside>
<footer>These communicate structure.
Instead of:
<div class="header">
...
</div>
<div class="navigation">
...
</div>
<div class="content">
...
</div>Prefer:
<header>
...
</header>
<nav>
...
</nav>
<main>
...
</main>The second version communicates meaning directly.
A good heading structure:
<h1>HTML Accessibility</h1>
<h2>Keyboard Navigation</h2>
<h2>Screen Readers</h2>
<h2>Accessible Forms</h2>
<h3>Labels</h3>
<h3>Error Messages</h3>Think of it as a document outline:
H1 HTML Accessibility
│
├── H2 Keyboard Navigation
│
├── H2 Screen Readers
│
└── H2 Accessible Forms
│
├── H3 Labels
└── H3 Error MessagesBad:
<h1>My article</h1>
<h4>Important section</h4>simply because you want the second heading to look smaller.
CSS should control appearance.
<h2>Important section</h2>h2 {
font-size: 1.2rem;
}Meaning comes first.
A page generally benefits from having a clear primary heading:
<h1>HTML Accessibility Guide</h1>Then:
<h2>Keyboard Navigation</h2>
<h2>Forms</h2>
<h2>Images</h2>The exact number of h1 elements isn't the main accessibility issue; what matters most is a clear, logical heading structure.
For a typical document, one primary h1 is a straightforward pattern.
alt#You learned this in Chapter 4.
For meaningful images:
<img
src="developer.jpg"
alt="Developer working on a laptop">The alt text communicates the image's purpose/content when the image can't be perceived.
Suppose an image is purely decorative:
✨✨✨✨It doesn't provide meaningful information.
Use:
<img
src="decoration.svg"
alt="">The empty alt tells assistive technology that the image is decorative.
This is different from:
<img src="decoration.svg">Missing alt and empty alt are not the same thing.
Bad:
Image contains:
"50% OFF — Sale ends today"with:
<img src="sale.png" alt="banner">The important information should exist as actual text when possible.
Better:
<p>
<strong>50% OFF</strong>
— Sale ends today.
</p>If the image adds meaningful context, give it appropriate alternative text.
Forms are one of the most important accessibility areas.
Bad:
<input type="email" placeholder="Enter email">There is no persistent programmatic label.
Better:
<label for="email">
Email address
</label>
<input
id="email"
name="email"
type="email">Now:
Label
│
│ for="email"
↓
<input id="email">Suppose a screen reader encounters:
<label for="email">Email address</label>
<input id="email" type="email">It can understand the relationship:
Email address
↓
Input fieldWithout a label:
<input type="email">the purpose may be unclear.
Another valid pattern:
<label>
Email address
<input
type="email"
name="email">
</label>The label and control are associated because the input is inside the label.
Both patterns are valid.
Explicit association is especially useful when you need a label and input separated in the markup:
<label for="email">Email</label>
<input id="email" name="email" type="email">This is a very common mistake.
Bad:
<input
type="email"
placeholder="Enter your email">The placeholder is a hint, not a replacement for the label.
Better:
<label for="email">
Email address
</label>
<input
id="email"
name="email"
type="email"
placeholder="you@example.com">Now:
Email address
[ you@example.com ]Placeholders can:
Use:
<label>for the field's name.
Use:
placeholder=""only as an optional hint/example.
Use:
<input
id="email"
name="email"
type="email"
required>The browser knows:
This field is required.You can also visually communicate required fields.
For example:
<label for="email">
Email address
<span aria-hidden="true">*</span>
</label>But don't rely solely on the * symbol to communicate the requirement.
Suppose you have:
Gender:
○ Male
○ Female
○ OtherA screen reader needs to understand that these radio buttons belong to the same group.
Use:
<fieldset>
<legend>Gender</legend>
<label>
<input
type="radio"
name="gender"
value="male">
Male
</label>
<label>
<input
type="radio"
name="gender"
value="female">
Female
</label>
<label>
<input
type="radio"
name="gender"
value="other">
Other
</label>
</fieldset>This gives the group a meaningful name.
Suppose:
Skills:
☐ JavaScript
☐ Python
☐ Java
☐ GoUse:
<fieldset>
<legend>Select your skills</legend>
<label>
<input
type="checkbox"
name="skills"
value="javascript">
JavaScript
</label>
<label>
<input
type="checkbox"
name="skills"
value="python">
Python
</label>
<label>
<input
type="checkbox"
name="skills"
value="java">
Java
</label>
</fieldset>This is one of the most important concepts in accessibility.
Use a link when the user is navigating:
<a href="/profile">
View profile
</a>Use a button when the user is performing an action:
<button type="button">
Delete account
</button>Think:
Navigate somewhere?
↓
<a>
Perform an action?
↓
<button>Bad:
<a href="#" onclick="deleteAccount()">
Delete
</a>This creates semantic and interaction problems.
Better:
<button type="button" onclick="deleteAccount()">
Delete
</button>Bad:
<button onclick="location.href='/about'">
About
</button>Better:
<a href="/about">
About
</a>Choose the element based on meaning, not appearance.
You can make a link look like a button:
.button-link {
display: inline-block;
padding: 10px 16px;
border-radius: 6px;
}But:
<a href="/checkout" class="button-link">
Checkout
</a>is still a link.
Its visual appearance doesn't change its semantic role.
A website should be usable without a mouse.
Common keyboard keys:
Tab
Shift + Tab
Enter
Space
Arrow keys
EscapeFor example:
Tab
↓
Logo link
↓
Home
↓
Products
↓
Search
↓
Add to cartUsers should be able to understand where keyboard focus is.
When an element receives keyboard focus, browsers commonly provide a visual focus indicator.
For example:
Normal:
[ Submit ]
Focused:
[ Submit ] ← visible focus indicatorDon't remove it without providing an equivalent accessible focus style.
Bad CSS:
button:focus {
outline: none;
}This can make keyboard navigation extremely difficult.
For example:
button:focus-visible,
a:focus-visible {
outline: 3px solid currentColor;
outline-offset: 3px;
}focus-visible is particularly useful because it allows you to style focus when it is relevant to the user's interaction mode.
tabindex#You may encounter:
tabindex="0"and:
tabindex="-1"tabindex="0"#Adds an element to the normal sequential keyboard navigation order.
<div tabindex="0">
Custom control
</div>But this doesn't automatically make a div behave like a button.
tabindex="-1"#The element isn't normally reached by sequential Tab navigation, but can receive programmatic focus.
element.focus();tabindex#Avoid:
tabindex="1"
tabindex="2"
tabindex="3"Positive tabindex values can create a confusing custom focus order.
Prefer natural DOM order:
<header>
...
</header>
<main>
...
</main>and let the browser manage normal keyboard traversal.
Consider:
<div class="left">
Navigation
</div>
<div class="right">
Main content
</div>CSS may visually put the main content first.
But keyboard and assistive technology navigation generally follows the document structure.
So:
Make the HTML order meaningful before using CSS to change visual layout.
Imagine a page with:
Logo
Navigation
50 navigation links
Social links
Ads
Main contentA keyboard user shouldn't have to press Tab dozens of times before reaching the content.
A skip link can help:
<a href="#main-content" class="skip-link">
Skip to main content
</a>Then:
<main id="main-content">
...
</main>Conceptually:
Tab
↓
Skip to main content
↓
Enter
↓
Main contentThis is extremely useful on complex websites.
Use:
<nav aria-label="Main navigation">
<ul>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="/blog">Blog</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
</ul>
</nav>If there are multiple navigation regions:
<nav aria-label="Main navigation">
...
</nav>
<nav aria-label="Footer navigation">
...
</nav>This gives assistive technology useful context.
aria-current#Suppose the current page is Blog:
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a
href="/blog"
aria-current="page">
Blog
</a>
<a href="/contact">
Contact
</a>
</nav>aria-current="page" communicates:
This link represents the current page.
Other values exist for other contexts, such as:
page
step
location
date
time
true
falseARIA stands for:
Accessible Rich Internet Applications
It provides additional accessibility semantics through attributes such as:
aria-label
aria-labelledby
aria-describedby
aria-hidden
aria-expanded
aria-controls
aria-current
aria-liveBut remember:
ARIA should complement HTML, not replace semantic HTML.
A very useful principle:
If a native HTML element already provides the required semantics, prefer the native element.
For example:
Don't do:
<div role="button">
Save
</div>when you can do:
<button>
Save
</button>Native HTML is usually simpler and more robust.
aria-label#You can give an accessible name using:
<button aria-label="Close">
×
</button>The visible symbol is:
×But assistive technology can identify the control as:
Closearia-label Is Useful#Consider an icon-only button:
<button>
🔍
</button>The meaning may not be sufficiently clear.
Better:
<button aria-label="Search">
🔍
</button>Another:
<button aria-label="Close dialog">
×
</button>aria-label#Don't write:
<h1 aria-label="HTML Tutorial">
HTML Tutorial
</h1>when the visible text already provides the accessible name.
Prefer:
<h1>
HTML Tutorial
</h1>ARIA should solve an actual semantic/accessibility need.
aria-labelledby#Suppose a dialog has:
<h2 id="dialog-title">
Delete account
</h2>You can associate the dialog with that visible heading:
<div
role="dialog"
aria-labelledby="dialog-title">
<h2 id="dialog-title">
Delete account
</h2>
</div>Now the accessible name comes from the visible heading.
This is often preferable to duplicating the text in aria-label.
aria-describedby#Suppose:
<label for="password">
Password
</label>
<input
id="password"
type="password"
aria-describedby="password-help">
<p id="password-help">
Password must contain at least 8 characters.
</p>The relationship is:
Password
│
↓
Input
│
↓
Additional description
│
↓
password-helpAssistive technology can use the description to provide additional context.
aria-hidden#Example:
<span aria-hidden="true">
✨
</span>This can hide an element from the accessibility tree.
Useful for decorative content that doesn't need to be announced.
For example:
<button>
<span aria-hidden="true">🔍</span>
Search
</button>The visible icon is decorative because the text already says:
SearchBad:
<p aria-hidden="true">
Your payment failed.
</p>If the user needs this information, don't hide it from assistive technologies.
aria-hidden="true" should be used intentionally.
aria-expanded#Useful for expandable controls.
Example:
<button
aria-expanded="false"
aria-controls="menu">
Menu
</button>
<nav id="menu" hidden>
...
</nav>When open:
<button
aria-expanded="true"
aria-controls="menu">
Menu
</button>The attribute communicates state:
false → collapsed
true → expandedaria-controls#This identifies the element controlled by a control.
<button
aria-expanded="false"
aria-controls="mobile-menu">
Menu
</button>
<nav id="mobile-menu">
...
</nav>Relationship:
Button
│
│ aria-controls
↓
mobile-menuJavaScript would normally update the state and visibility.
hidden#HTML provides a native mechanism:
<div hidden>
Hidden content
</div>The browser treats the element as hidden.
For a menu:
<nav id="mobile-menu" hidden>
...
</nav>Then JavaScript can remove hidden when opening it.
This is often preferable to using ARIA alone to indicate visibility.
This is a critical concept.
If you write:
<button
aria-expanded="false"
aria-controls="menu">
Menu
</button>ARIA communicates state.
It does not automatically create the menu behavior.
You still need appropriate HTML/CSS/JavaScript to:
Click Menu
↓
Open menu
↓
Update aria-expanded
↓
Show menuARIA describes accessibility state; it doesn't implement the interaction.
A simplified example:
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title">
<h2 id="dialog-title">
Delete account
</h2>
<p>
Are you sure you want to delete your account?
</p>
<button type="button">
Cancel
</button>
<button type="button">
Delete account
</button>
</div>But a truly accessible modal also requires proper JavaScript focus management.
For example:
Open dialog
↓
Move focus into dialog
↓
User interacts
↓
Escape can close when appropriate
↓
Restore focus to triggering elementARIA alone isn't enough.
You learned tables earlier.
Use:
<table>
<caption>Employee salaries</caption>
<thead>
<tr>
<th scope="col">Employee</th>
<th scope="col">Department</th>
<th scope="col">Salary</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Anubhav</th>
<td>Engineering</td>
<td>₹26.5 LPA</td>
</tr>
</tbody>
</table>Important accessibility features:
caption
th
scope
logical structureBad:
<a href="/article">Click here</a>Better:
<a href="/article">
Read the HTML accessibility guide
</a>Why?
A screen reader user may navigate through links separately.
Imagine hearing:
Link, click here
Link, click here
Link, click hereThat's not useful.
Instead:
Link, Read the HTML accessibility guide
Link, Download the PDF guide
Link, View the pricing pageThe destination is clear.
If multiple cards contain:
<a href="/article-1">Read more</a>
<a href="/article-2">Read more</a>
<a href="/article-3">Read more</a>The visible text is ambiguous when links are encountered out of context.
Better:
<a href="/article-1">
Read more about AI agents
</a>
<a href="/article-2">
Read more about RAG
</a>
<a href="/article-3">
Read more about vector databases
</a>Or provide the article title as the link itself.
Suppose:
<button>
❤️
</button>What does it mean?
Maybe:
Like
Favorite
Remove
DeleteMake the purpose clear:
<button aria-label="Add to favorites">
❤️
</button>Or:
<button>
<span aria-hidden="true">❤️</span>
Add to favorites
</button>Bad:
Red = Error
Green = SuccessSome users may not distinguish colors reliably.
Better:
❌ Error: Password is too short.and:
✓ Account created successfully.The color can still reinforce the meaning, but shouldn't be the only way the information is communicated.
Bad:
Email field turns red.That's not enough.
Better:
<label for="email">
Email address
</label>
<input
id="email"
name="email"
type="email"
aria-describedby="email-error">
<p id="email-error">
Enter a valid email address.
</p>Now the error is actual text.
For dynamic validation, you may also need appropriate focus management and live-region techniques depending on the interaction.
aria-live#aria-live can communicate dynamically updated content to assistive technologies.
Example:
<div aria-live="polite">
</div>JavaScript might later insert:
"Your profile has been saved."The browser/assistive technology can announce the update depending on the live-region configuration and assistive technology behavior.
Common values:
off
polite
assertiveUse assertive carefully because interrupting a user's current speech can be disruptive.
A search form can be:
<form role="search" action="/search" method="get">
<label for="search">
Search
</label>
<input
id="search"
name="q"
type="search">
<button type="submit">
Search
</button>
</form>Because the form has:
role="search"assistive technology can identify it as a search landmark.
input type="search"#HTML provides:
<input type="search">Use it when the field represents a search query.
<label for="site-search">
Search
</label>
<input
id="site-search"
name="q"
type="search">This is more meaningful than simply:
<input type="text">when the field's purpose is searching.
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>
<a href="/" aria-label="NextGen Blog home">
NextGen Blog
</a>
<nav aria-label="Main navigation">
<ul>
<li>
<a href="/" aria-current="page">
Home
</a>
</li>
<li>
<a href="/ai">
AI
</a>
</li>
<li>
<a href="/web-development">
Web Development
</a>
</li>
<li>
<a href="/contact">
Contact
</a>
</li>
</ul>
</nav>
</header>
<main id="main-content">
...
</main>Notice how many accessibility features come simply from using good HTML.
The browser doesn't only create a DOM.
Conceptually:
HTML
↓
DOM
↓
Accessibility Tree
↓
Assistive TechnologyFor example:
<button>
Save
</button>The browser can expose something conceptually like:
Role: button
Name: Save
State: enabledA screen reader can then announce something like:
"Save, button"The exact announcement depends on the browser and assistive technology.
Think:
HTML
│
┌───────┴───────┐
↓ ↓
DOM Accessibility Tree
│ │
↓ ↓
Web application Assistive techSemantic HTML helps the browser build a meaningful accessibility representation.
You may encounter:
<div role="button">or:
<div role="dialog">or:
<div role="alert">ARIA roles communicate semantics.
Examples:
button
dialog
alert
navigation
search
tab
tabpanel
statusBut again:
Don't use ARIA to recreate native HTML unnecessarily.
Prefer:
<button>over:
<div role="button">A useful progression is:
1. Native HTML
↓
2. HTML + CSS
↓
3. HTML + CSS + JavaScript
↓
4. ARIA only when necessaryNot:
<div>
<div>
<div role="button">
<div aria-label="...">
<div tabindex="0">when native elements could solve the problem.
Before shipping a page, check:
✓ Correct language
✓ Logical headings
✓ Semantic elements
✓ Meaningful landmarks✓ Keyboard accessible
✓ Visible focus
✓ Logical tab order
✓ Skip link where useful✓ Labels
✓ Instructions
✓ Errors
✓ Required state
✓ Group related controls✓ Meaningful alt
✓ Decorative alt=""
✓ No important information only in images✓ Native buttons
✓ Native links
✓ Keyboard operation
✓ Accessible names✓ State communicated
✓ Focus managed
✓ Dynamic updates announced when necessaryDon't just inspect the source code.
Test with:
Unplug your mouse or simply don't use it.
Try:
Tab
Shift + Tab
Enter
Space
Arrow keys
EscapeCan you complete the task?
Common screen readers include:
NVDA
JAWS
VoiceOver
TalkBackYou can test how your page is announced.
Modern browser developer tools provide accessibility inspection features.
You can inspect:
Role
Name
State
PropertiesTools such as:
axe
Lighthouse
Accessibility Insightscan identify many common problems.
But automated tools don't detect everything.
Suppose an automated scanner says:
100% accessibilityThat does not prove the website is fully accessible.
Why?
A tool may detect:
Missing alt
Missing label
Color contrast
Invalid ARIABut it may not understand whether:
The keyboard flow makes sense.
The error message is understandable.
The interaction is confusing.
The focus moves correctly.
The content itself is clear.Human testing still matters.
You will often hear:
WCAG
It stands for:
Web Content Accessibility Guidelines
WCAG provides internationally recognized accessibility guidance.
The four high-level principles are:
P
O
U
RUsers must be able to perceive the information.
Users must be able to operate the interface.
Users must be able to understand the content and interaction.
Content should work with different user agents and assistive technologies.
Imagine a video:
Provide captions:
<track kind="captions">Allow keyboard controls.
Use clear controls.
Use proper HTML and standards so assistive technologies can interpret it.
Here's a practical example combining several concepts:
<form action="/register" method="post">
<h1>Create an Account</h1>
<div>
<label for="name">
Full name
</label>
<input
id="name"
name="name"
type="text"
autocomplete="name"
required>
</div>
<div>
<label for="email">
Email address
</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
required
aria-describedby="email-help">
<p id="email-help">
We'll use this address for account notifications.
</p>
</div>
<div>
<label for="password">
Password
</label>
<input
id="password"
name="password"
type="password"
autocomplete="new-password"
minlength="8"
required
aria-describedby="password-help">
<p id="password-help">
Password must contain at least 8 characters.
</p>
</div>
<fieldset>
<legend>Preferred contact method</legend>
<label>
<input
type="radio"
name="contact"
value="email"
checked>
Email
</label>
<label>
<input
type="radio"
name="contact"
value="phone">
Phone
</label>
</fieldset>
<label>
<input
type="checkbox"
name="terms"
required>
I agree to the terms and conditions.
</label>
<button type="submit">
Create account
</button>
</form>Notice how much accessibility we get from native HTML:
label
fieldset
legend
button
input
required
autocomplete
aria-describedby<div onclick="openMenu()">
Menu
</div><button type="button">
Menu
</button><img src="profile.jpg"><img
src="profile.jpg"
alt="Anubhav smiling"><input placeholder="Email"><label for="email">
Email
</label>
<input
id="email"
placeholder="you@example.com">*:focus {
outline: none;
}Provide a clear focus indicator.
<a href="/guide">
Click here
</a><a href="/guide">
Read the HTML accessibility guide
</a>The practice of making websites usable by people with different abilities and assistive technologies.
A shorthand for accessibility.
Because it communicates the meaning and structure of content to browsers, assistive technologies, developers, and other tools.
alt="" and missing alt?#alt=""explicitly identifies the image as having no alternative text, commonly appropriate for decorative images.
A missing alt does not communicate that intentional decision and can lead to poor or inconsistent accessibility behavior.
Because placeholders are temporary hints and are not a reliable replacement for a persistent field label.
<button> instead of <a>?#Use <button> for actions and <a> for navigation.
Accessible Rich Internet Applications — a set of attributes that provide additional accessibility semantics and states.
No.
Prefer native semantic HTML whenever it provides the required behavior and semantics.
aria-label?#It provides an accessible name for an element when appropriate.
Example:
<button aria-label="Close">
×
</button>aria-labelledby?#It associates an element with another element that provides its accessible name.
aria-describedby?#It associates an element with additional descriptive content.
aria-expanded?#It communicates whether an expandable control is currently expanded or collapsed.
A link that allows keyboard users to bypass repetitive navigation and move directly to the main content.
A browser-generated representation of relevant UI semantics that assistive technologies can consume.
Build:
accessible-blog.htmlStructure:
Skip to main content
↓
Header
↓
Accessible navigation
↓
Main
├── Article
│ ├── H1
│ ├── Author
│ ├── Image
│ ├── Content
│ ├── H2
│ ├── H2
│ └── Related links
│
└── Aside
└── Related articles
↓
FooterRequirements:
✓ Semantic HTML
✓ Correct headings
✓ lang attribute
✓ Accessible navigation
✓ Meaningful image alt
✓ Accessible links
✓ Skip link
✓ Main landmark
✓ Aside
✓ Footer✓ Keyboard navigation
✓ Visible focus
✓ No clickable divs
✓ Buttons used for actions
✓ Links used for navigationAdd:
✓ Search form
✓ Label
✓ Search input
✓ Submit buttonTry to use the entire website:
WITHOUT A MOUSEThat is one of the best beginner accessibility exercises.
When writing HTML, ask:
HTML Element
│
↓
What does it mean?
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Browser Screen Reader Keyboard
│ │ │
└──────────────┼──────────────┘
↓
User ExperienceThe most important rule is:
Choose HTML based on meaning, not appearance.
Use:
<button>when it's a button.
Use:
<a>when it's a link.
Use:
<nav>for navigation.
Use:
<main>for the main content.
Use:
<label>for form controls.
Use:
<h1>–<h6>for document headings.
Use ARIA when native HTML doesn't adequately express the required semantics.
That single principle will prevent a huge number of accessibility problems.
Next we'll learn how HTML works with responsive design, including:
srcsetsizes<picture>Mark it as complete to track your mastery.