How to Make a Website Screen Reader Friendly
A practical guide to semantic HTML, alt text, labels, keyboard access, ARIA, and testing a website with screen readers.
Make the page understandable through its structure, names, states and keyboard behavior. Use semantic HTML for headings, landmarks, lists, links, forms and buttons; provide purposeful text alternatives; set a useful title and language; keep every action keyboard-operable; and use ARIA only when native HTML cannot express the required behavior. Then review the page with keyboard checks, a screen reader and a broader WCAG evaluation.
This guide follows the W3C/WAI structure, development, keyboard, ARIA and evaluation guidance. A checklist or automated scan is useful for finding problems, but it cannot prove that every user can complete every task.
1. Start with a meaningful document structure
Screen-reader users often navigate by headings and regions instead of reading every line. Build the outline with elements that carry meaning rather than generic containers styled to look meaningful. W3C’s Page Structure Tutorial covers headings, regions and ways to reach the main content.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Account settings | Example Store</title>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>
<a href="/" aria-label="Example Store home">Example Store</a>
<nav aria-label="Primary">...</nav>
</header>
<main id="main-content">
<h1>Account settings</h1>
<section aria-labelledby="profile-heading">
<h2 id="profile-heading">Profile</h2>
...
</section>
<section aria-labelledby="notifications-heading">
<h2 id="notifications-heading">Notifications</h2>
...
</section>
</main>
<footer>...</footer>
</body>
</html>
- Give each page a concise, specific
<title>. - Set the primary document language with
lang. - Use one page-level
h1for the subject, then headings for conceptual sections. Do not skip levels just to achieve a visual size. - Use
header,nav,main,asideandfooterwhen they describe the content. Label repeated regions when users need to distinguish them. - Make the skip link visible when focused and ensure its target can receive focus or is otherwise reached reliably.
2. Write useful text alternatives
Alternative text should communicate an image’s purpose or essential information in context. W3C’s Images Tutorial distinguishes informative, functional, decorative and complex images.
<!-- Informative image -->
<img src="revenue-trend.png" alt="Revenue rose from $42,000 in January to $57,000 in March.">
<!-- Functional image button -->
<button type="submit" aria-label="Search">
<img src="search.svg" alt="">
</button>
<!-- Decorative image -->
<img src="confetti.svg" alt="">
<!-- Complex chart: brief alternative plus nearby detail -->
<figure>
<img src="conversion-chart.png" alt="Conversion rate by month; detailed data follows.">
<figcaption>January 2.1%, February 2.8%, March 3.4%.</figcaption>
</figure>
Do not prepend “image of” to every alternative. A decorative image normally uses alt="" so it is omitted from the accessibility tree. An icon that performs an action needs the action as its name, such as “Search,” rather than “magnifying glass.”
3. Give controls names and links context
Associate a visible label with every form control. The label should explain what value is expected, not merely repeat a nearby heading. Links should remain understandable when read in a list without surrounding prose.
<form>
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<fieldset>
<legend>Delivery frequency</legend>
<label><input type="radio" name="frequency" value="weekly"> Weekly</label>
<label><input type="radio" name="frequency" value="monthly"> Monthly</label>
</fieldset>
<button type="submit">Save notification settings</button>
</form>
<p><a href="/pricing">View pricing plans</a></p>
Use fieldset and legend for related choices. Use a real button for an action and a real link for navigation. Avoid repeated “Read more” or “Click here” labels unless another programmatic mechanism makes each purpose clear. Use aria-label, aria-labelledby or descriptions only when native visible labeling is insufficient.
4. Make every interaction keyboard accessible
W3C’s keyboard guidance states: “Make all functionality available from a keyboard.” Test with Tab, Shift+Tab, Enter, Space and arrow keys where a widget requires them. Focus must move in a sensible order, remain visible, and never become trapped.
/* Keep focus visible; do not remove the browser indicator. */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 3px;
}
/* A skip link can be visually hidden until focused. */
.skip-link {
position: absolute;
left: 1rem;
top: -4rem;
}
.skip-link:focus {
top: 1rem;
}
- Do not use
divorspanas a control without implementing its complete keyboard behavior. - Do not set
tabindexto positive values; they create a confusing focus order. Usetabindex="0"only when a custom focusable element truly needs it, andtabindex="-1"for programmatic focus targets. - When opening a dialog, move focus into it, keep focus inside while open, provide an Escape path, and return focus to the trigger on close.
- Ensure menus, tabs, comboboxes and grids follow an established keyboard pattern.
5. Use ARIA to complete custom and dynamic controls
WAI-ARIA Authoring Practices documents widget roles, states, properties and keyboard behavior. Prefer native HTML when it already supplies the required semantics. An ARIA role does not create interaction by itself: a custom button still needs keyboard handling, focus styling and an activated state.
<button type="button"
aria-expanded="false"
aria-controls="filters-panel"
id="filters-toggle">
Filters
</button>
<section id="filters-panel" hidden aria-labelledby="filters-toggle">
...
</section>
<div role="status" aria-live="polite"></div>
Keep the DOM state and ARIA state synchronized. Use a polite live region for non-urgent updates such as “Saved.” Reserve assertive announcements for interruptions that truly require immediate attention. For a custom widget, implement the complete role, name, value, state and keyboard contract described by its pattern.
6. Handle common page and application edge cases
- Single-page applications: update the document title and move focus to the new view heading after route changes.
- Validation: identify the invalid field, expose the error in text, connect it with
aria-describedby, and provide a summary that links to each error. - Loading: announce meaningful completion or failure, and do not leave focus on a removed element.
- Hidden content: use the
hiddenattribute or appropriate CSS when content should be unavailable; do not merely make text transparent or move it off-screen. - Motion and contrast: preserve readable focus indicators and provide alternatives for motion that can distract or trigger symptoms.
- Language changes: mark passages in another language with a matching
langvalue when pronunciation matters.
7. Review the implementation in layers
Begin with W3C/WAI’s Easy Checks: title, headings, alternatives, labels, keyboard access and visible structure. Compare what is visible with what assistive technology can discover.
- Keyboard pass: complete every task without a mouse. Record skipped controls, invisible focus, traps and unexpected focus jumps.
- Structure pass: inspect the heading and landmark lists in a screen reader. Confirm names and hierarchy match the page’s purpose.
- Task pass: use a screen reader appropriate to the target platform and complete representative flows: navigation, search, form errors, dialogs, dynamic updates and checkout.
- Code and automated pass: use validators and accessibility tooling to catch missing labels, invalid ARIA and obvious contrast or state errors. Treat results as leads, not certification.
- Evaluation pass: for a product-wide review, follow the scope and sampling process in WCAG-EM. Include people who use screen readers when possible.
NV Access provides NVDA, a free, open-source screen reader for supported platforms. Choose tools that match your users’ devices; no single screen reader represents every environment.
8. Capture visual evidence without mistaking it for an accessibility test
Screenshots help document focus indicators, responsive layouts, error messages and visual regressions. They cannot expose an unlabeled control or prove that a screen-reader task works, so pair them with the structural and task reviews above.
Or skip the browser setup
ScreenshotNeo captures a page with one GET request and returns PNG, JPEG, WebP or PDF. Its clean-shot steps accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups and chat widgets before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the verdict and billing status.
See the ScreenshotNeo API documentation for all options, including full-page lazy-image loading, CSS-selector element capture, dark mode, device presets, custom viewport and retina scale, custom CSS and JavaScript, click and wait actions, hidden selectors, request blocking, headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous webhooks, bulk capture and usage reporting.
# cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o accessibility-review.webp
# Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
open("accessibility-review.webp", "wb").write(r.content)
// Node.js 18+
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('accessibility-review.webp', Buffer.from(await res.arrayBuffer()));
For repeatable review captures, choose a fixed viewport, device preset and wait condition; use full-page mode when checking long forms; hide transient chat widgets; and set a cache TTL only when the page is intentionally stable. Signed links are suitable for public img tags, while asynchronous jobs and signed webhooks fit slow pages or bulk runs.
ScreenshotNeo includes an MCP server with take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. Only clean shots are billed, which helps avoid paying for failed captures.
Sign up for 1,000 free screenshots a month with no card.
9. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| Headings do not appear in a useful outline | Visual styling replaces semantic headings or levels are chosen for size | Use real h1–h6 elements and adjust CSS for appearance. |
| Image is announced twice | Decorative image has non-empty alt or adjacent text repeats it |
Use alt="" for decoration and remove redundant wording. |
| Control has no accessible name | Icon-only button or input lacks a label | Add visible label, button text, or a correctly referenced naming attribute. |
| Keyboard focus disappears | CSS removes outlines or a script moves focus to a removed node | Restore :focus-visible styling and manage focus after DOM updates. |
| Custom widget is reachable but unusable | ARIA role was added without states or keyboard behavior | Use native HTML or implement the complete APG pattern. |
| Dynamic update is missed | No live-region announcement or update is too noisy | Add a suitably polite live region and announce only meaningful changes. |
| Screenshot shows a banner or blank page | Consent overlay, bot check, slow resource or failed navigation | For DIY capture, wait for the real content and handle overlays. With ScreenshotNeo, clean-shot handling removes supported consent, popup and chat overlays; inspect X-Page-Verdict and X-Billed headers. |
| Screenshot request times out | Target page or network resources are slow | Use a selector or network-idle wait, block unnecessary resource types, or use an asynchronous job. |
10. Performance, reliability and maintenance
- Prefer semantic server-rendered HTML where practical; it gives assistive technology a usable structure before client scripts finish.
- Keep focus and live-region updates small and intentional. Excess announcements slow task completion.
- Test representative templates and critical flows after every navigation, form, modal or component change.
- For screenshot evidence, reuse stable capture parameters and cache only pages whose content may safely be reused. Use bulk capture for up to 100 URLs per call and asynchronous jobs for slow pages.
- Check verdict and billing headers so monitoring distinguishes a clean capture from a bot check, blank page, timeout, failed load or cache hit.
FAQ
Can semantic HTML alone make a site accessible?
No. It supplies a strong foundation, but names, keyboard behavior, focus, dynamic updates, contrast, content and task flows still need review.
Do I need to use ARIA on every element?
No. Use native HTML first. Add ARIA when a custom or dynamic interface needs semantics that HTML does not provide, and implement the associated behavior.
Is a screenshot an accessibility audit?
No. It records visual output. Pair it with keyboard, screen-reader and broader WCAG evaluation.
Should every image have descriptive alt text?
Every meaningful image needs an appropriate alternative. Decorative images generally need an empty alternative so they do not add redundant speech.
Further reading
A Web for Everyone by Sarah Horton and Whitney Quesenbery is broader accessible-UX reading. It was published in 2014, so use current W3C guidance for standards and evaluation practice.


