56 results available
Sign in
Skills directory

screen-reader-testing

wshobson

·

Last changed
Hardened

This skill has been hardened by Chainguard

Last changed

This version passed the hardening gate. Hardening updated the skill content and independently checked the resulting changes. Classified findings before hardening: 0. Classified findings closed: 0. Classified findings remaining: 0. Unclassified scanner findings remaining: 0. Review findings closed: 8. Review findings remaining: 3.


Open before hardening

None

Open after hardening

None


Expand
File
Group
Subtype
Severity
Status

No findings available

0 findings


SKILL.md

Screen Reader Testing

Practical guide to testing web applications with screen readers for comprehensive accessibility validation.

When to Use This Skill

  • Validating screen reader compatibility
  • Testing ARIA implementations
  • Debugging assistive technology issues
  • Verifying form accessibility
  • Testing dynamic content announcements
  • Ensuring navigation accessibility

Core Concepts

1. Major Screen Readers

Screen ReaderPlatformBrowser

VoiceOver

macOS/iOS

Safari

NVDA

Windows

Firefox/Chrome

JAWS

Windows

Chrome

TalkBack

Android

Chrome

Narrator

Windows

Edge

2. Testing Priority

Minimum Coverage:
1. NVDA + Firefox (Windows)
2. VoiceOver + Safari (macOS)
3. VoiceOver + Safari (iOS)

Comprehensive Coverage:
+ JAWS + Chrome (Windows)
+ TalkBack + Chrome (Android)
+ Narrator + Edge (Windows)

3. Screen Reader Modes

ModePurposeWhen Used

Browse/Virtual

Read content

Default reading

Focus/Forms

Interact with controls

Filling forms

Application

Custom widgets

ARIA applications

Before testing on macOS, read VoiceOver setup, commands, and checklists.

For the required iOS Safari coverage, read VoiceOver touch setup and gestures.

Before testing with NVDA, read NVDA setup, modes, commands, and test scripts.

Before testing with JAWS or TalkBack, read platform setup and commands.

These reference guides are for the human tester. Get the user's confirmation before installing software or changing system settings based on them.

Common Test Scenarios

1. Modal Dialog

<!-- Accessible modal structure -->
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <h2 id="dialog-title" tabindex="-1">Confirm Delete</h2>
  <p id="dialog-desc">This action cannot be undone.</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>
// Focus management
function openModal(modal) {
  // Store last focused element
  lastFocus = document.activeElement;

  // Move focus to modal
  modal.querySelector("h2").focus();

  // Trap focus
  modal.addEventListener("keydown", trapFocus);
}

function closeModal(modal) {
  // Return focus
  lastFocus.focus();
}

function trapFocus(e) {
  if (e.key === "Tab") {
    const focusable = modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      last.focus();
      e.preventDefault();
    } else if (!e.shiftKey && document.activeElement === last) {
      first.focus();
      e.preventDefault();
    }
  }

  if (e.key === "Escape") {
    closeModal(modal);
  }
}

This is a simplified sketch, not copy-ready code. In real code, keep the modal in scope inside the keydown handler, declare the stored focus variable, remove the keydown listener and hide the dialog on close, and check that the stored element still exists before returning focus. The focusable list should also leave out disabled controls and hidden or inert elements. Test the result with a real screen reader before relying on it.

2. Live Regions

<!-- Status messages (polite) -->
<div role="status" aria-live="polite" aria-atomic="true">
  <!-- Content updates will be announced after current speech -->
</div>

<!-- Alerts (assertive) -->
<div role="alert" aria-live="assertive">
  <!-- Content updates interrupt current speech -->
</div>

<!-- Progress updates -->
<div
  role="progressbar"
  aria-valuenow="75"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Upload progress"
></div>

<!-- Log (additions only) -->
<div role="log" aria-live="polite" aria-relevant="additions">
  <!-- New messages announced, removals not -->
</div>

3. Tab Interface

<div role="tablist" aria-label="Product information">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
    Description
  </button>
  <button
    role="tab"
    id="tab-2"
    aria-selected="false"
    aria-controls="panel-2"
    tabindex="-1"
  >
    Reviews
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  Product description content...
</div>

<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Reviews content...
</div>
// Tab keyboard navigation
tablist.addEventListener("keydown", (e) => {
  const tabs = [...tablist.querySelectorAll('[role="tab"]')];
  const index = tabs.indexOf(document.activeElement);

  let newIndex;
  switch (e.key) {
    case "ArrowRight":
      newIndex = (index + 1) % tabs.length;
      break;
    case "ArrowLeft":
      newIndex = (index - 1 + tabs.length) % tabs.length;
      break;
    case "Home":
      newIndex = 0;
      break;
    case "End":
      newIndex = tabs.length - 1;
      break;
    default:
      return;
  }

  tabs[newIndex].focus();
  activateTab(tabs[newIndex]);
  e.preventDefault();
});

The activateTab call is a placeholder you supply. It should update aria-selected on each tab, leave only the active tab in the tab order using tabindex, and show the matching panel while hiding the others.

Debugging Tips

The helper below is only a rough approximation of what a screen reader announces. The aria-labelledby attribute holds ID references rather than text, and it takes precedence over aria-label. The helper also ignores label elements, alt text and title, reports tag names that are not the same as implicit roles, and ignores aria-hidden on the element and its ancestors. Use the accessibility tree in the browser developer tools for the real computed name and role.

// Log what screen reader sees
function logAccessibleName(element) {
  const computed = window.getComputedStyle(element);
  console.log({
    role: element.getAttribute("role") || element.tagName,
    name:
      element.getAttribute("aria-label") ||
      element.getAttribute("aria-labelledby") ||
      element.textContent,
    state: {
      expanded: element.getAttribute("aria-expanded"),
      selected: element.getAttribute("aria-selected"),
      checked: element.getAttribute("aria-checked"),
      disabled: element.disabled,
    },
    visible: computed.display !== "none" && computed.visibility !== "hidden",
  });
}

Best Practices

Do's

  • Test with actual screen readers - Not just simulators
  • Use semantic HTML first - ARIA is supplemental
  • Test in browse and focus modes - Different experiences
  • Verify focus management - Especially for SPAs
  • Test keyboard only first - Foundation for SR testing

Don'ts

  • Don't assume one SR is enough - Test multiple
  • Don't ignore mobile - Growing user base
  • Don't test only happy path - Test error states
  • Don't skip dynamic content - Most common issues
  • Don't rely on visual testing - Different experience

Install

Install with chainctl
chainctl skill install public/wshobson/screen-reader-testing:latest
Get started with chainctl
View source

Harden your own skills

Bring Chainguard's hardening pipeline to your first-party skills — custom rulesets, automated remediation, full audit trail.

Provenance

Upstream sourcehttps://github.com/wshobson/agents
Upstream commit13703c9c803d69e6f80264f1f05d6d3da6552a6f
OCI reference

skills.cgr.dev/public/github.com/wshobson/plugins/accessibility-compliance/skills/screen-reader-testing:latest

License

MIT

Files

HARDENING.md

LICENSE

SKILL.md

references/jaws-talkback.md

references/nvda.md

references/voiceover-ios.md

references/voiceover.md


The trusted source for open source

Talk to an expert
PrivacyTerms

Product

Chainguard ContainersChainguard LibrariesChainguard VMsChainguard OS PackagesChainguard ActionsChainguard Agent SkillsIntegrationsPricing
© 2026 Chainguard, Inc. All Rights Reserved.
Chainguard® and the Chainguard logo are registered trademarks of Chainguard, Inc. in the United States and/or other countries.
The other respective trademarks mentioned on this page are owned by the respective companies and use of them does not imply any affiliation or endorsement.