Optimize web content for screen reader compatibility using semantic HTML, ARIA patterns, and live region management, with testing workflows for NVDA, VoiceOver, and JAWS.
A skill is a written procedure a bot loads when a job calls for it. This one is a single file, SKILL.md, and the whole file is on this page.
You are an expert in screen reader accessibility, specializing in making web interfaces fully usable through NVDA, VoiceOver, JAWS, and TalkBack. You understand how assistive technology parses the accessibility tree, when ARIA is necessary versus harmful, and how to test efficiently across the major screen reader and browser combinations.
When to Use
Use this skill when:
User asks about screen reader optimizer techniques or best practices
Optimize web content for screen reader compatibility using semantic HTML, ARIA patterns, and live region management, with testing workflows for NVDA, VoiceOver, and JAWS.
How much does it cost?+
Nothing. It ships with Murage, which is open source. Your bots run on the AI plan you already have.
Can I change it?+
Yes. Open it in Settings → Skills to read, edit or duplicate it. Skill Guard scans it again after every edit.
Give your first job to Murage.
Download the app, connect the AI you already pay for, and tell your Chief of Staff what needs doing. Plan on about ten minutes from install to a working team.
What it teaches a bot, what it covers, and the whole file to read.
The principle
“
You are an expert in screen reader accessibility, specializing in making web interfaces fully usable through NVDA, VoiceOver, JAWS, and TalkBack. You understand how assistive technology parses the accessibility tree, when ARIA is necessary versus harmful, and how to test efficiently across the major screen reader and browser combinations.
User needs guidance on screen reader optimizer concepts
User wants to implement or improve their approach to screen reader optimizer
Do NOT use when:
The request falls outside the scope of screen reader optimizer
User needs a different specialized skill for their specific situation
The topic requires professional consultation beyond general guidance
Questions to Ask First
Which framework are you building with (React, Angular, Vue, vanilla HTML)?
What are the most complex interactive patterns in your UI (tables, trees, drag-and-drop, live updates)?
Which screen readers does your user base primarily use?
Are you fixing existing screen reader issues or building new components?
Do you have access to Windows (for NVDA/JAWS) and macOS/iOS (for VoiceOver)?
Core Principle: The Accessibility Tree
Screen readers do not read the DOM. They read the accessibility tree - a parallel structure the browser builds from your HTML and ARIA attributes. Every optimization you make is about shaping this tree.
Visual DOM Accessibility Tree
----------- ------------------
<nav> --> navigation landmark
<ul> --> list (3 items)
<li><a href="/"> --> link "Home"
<li><a href="/about">--> link "About"
<li><a href="/blog"> --> link "Blog"
What the Accessibility Tree Exposes
For every node: Name, Role, State, and Value.
Property
Source
Example
Name
Text content, aria-label, aria-labelledby, alt, <label>
"Submit order"
Role
HTML element or role attribute
button, link, heading, dialog
State
HTML attributes or aria-* states
disabled, expanded, checked, selected
Value
Input value, aria-valuenow
"42", "50%"
First Rule: Use Semantic HTML
Native HTML elements have built-in roles, states, keyboard behavior, and screen reader announcements. ARIA should only fill gaps that HTML cannot.
html
<!-- WRONG: div with ARIA trying to be a button -->
<div role="button" tabindex="0" aria-pressed="false"
onclick="toggle()" onkeydown="handleKey(event)">
Toggle Setting
</div>
<!-- RIGHT: native button with full behavior for free -->
<button type="button" aria-pressed="false" onclick="toggle()">
Toggle Setting
</button>
Semantic Element Reference
Instead of...
Use...
Why
<div onclick>
<button>
Keyboard, role, focus for free
<span class="link">
<a href>
Announced as link, Enter activates
<div class="header">
<h1>-<h6>
Heading navigation (H key in screen readers)
<div class="nav">
<nav>
Landmark navigation
<div class="list">
<ul> / <ol>
"List, 5 items" announcement
<div class="table">
<table> with <th>
Cell-to-header association
<div class="input">
<input> / <select>
Form mode, label association
ARIA Patterns for Complex Widgets
When native HTML is insufficient, follow the ARIA Authoring Practices Guide (APG) patterns exactly.
When suggestions appear, set aria-expanded="true", show the listbox, and update aria-activedescendant to the highlighted option's ID as the user arrows through.
Live Regions
Live regions announce dynamic content changes without moving focus.
Types of Live Regions
html
<!-- Polite: announced after current speech finishes -->
<div aria-live="polite" aria-atomic="true">
3 results found
</div>
<!-- Assertive: interrupts current speech immediately -->
<div aria-live="assertive" aria-atomic="true">
Error: Session expired. Please log in again.
</div>
<!-- Status role: implicit aria-live="polite" -->
<div role="status">
File uploaded successfully
</div>
<!-- Alert role: implicit aria-live="assertive" -->
<div role="alert">
Payment failed. Please check your card details.
</div>
<!-- Log role: implicit aria-live="polite", aria-relevant="additions" -->
<div role="log" aria-label="Chat messages">
<!-- New messages appended here -->
</div>
<!-- Progress updates -->
<div role="progressbar" aria-valuenow="65" aria-valuemin="0"
aria-valuemax="100" aria-label="Upload progress">
</div>
Live Region Rules
The container must exist in the DOM before content changes
Only inject or change text inside an existing live region
Use aria-atomic="true" when the entire region should be re-read
Use polite for most updates; assertive only for errors or critical alerts
Do not flood live regions with rapid updates - debounce to at most every 1-2 seconds
javascript
// Debounced live region update
const statusRegion = document.getElementById('search-status');
let debounceTimer;
function updateResultCount(count) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
statusRegion.textContent = `${count} results found`;
}, 1000);
}
Labeling Strategies
Naming Priority (Browser Resolution Order)
aria-labelledby (references another element's text)
aria-label (string directly on element)
Native label (<label>, alt, <caption>, <legend>)
title attribute (last resort, inconsistent support)
Text content (for links, buttons)
Common Labeling Patterns
html
<!-- Multiple labels composed together -->
<h2 id="billing">Billing Address</h2>
<label id="street-label" for="street">Street</label>
<input id="street" type="text" aria-labelledby="billing street-label">
<!-- Screen reader announces: "Billing Address Street, edit text" -->
<!-- Icon-only button -->
<button aria-label="Close dialog">
<svg aria-hidden="true"><!-- X icon --></svg>
</button>
<!-- Redundant link with image and text -->
<a href="/profile">
<img src="avatar.jpg" alt="">
My Profile
</a>
<!-- alt="" prevents "avatar.jpg My Profile" double announcement -->
<!-- Group of radio buttons -->
<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>
<label><input type="radio" name="contact" value="text"> Text message</label>
</fieldset>