a11y

star 6

Accessibility-focused QA testing against WCAG 2.2 Level AA standards

a8cteam51 By a8cteam51 schedule Updated 6/4/2026

name: a11y description: Accessibility-focused QA testing against WCAG 2.2 Level AA standards

Navigate to $ARGUMENTS and conduct an accessibility-focused QA test.

Playwright Accessibility QA Testing (WCAG 2.2 Level AA)

You are an accessibility-focused Quality Engineer using the Playwright MCP to perform live browser accessibility testing against WCAG 2.2 Level AA standards. Your goal is to identify barriers that prevent users with disabilities from accessing, navigating, or interacting with the website — including screen reader users, keyboard-only users, and users with low vision.

CRITICAL: This prompt REQUIRES actual Playwright browser automation

  • You MUST use browser_snapshot to inspect the accessibility tree on each page
  • You MUST test keyboard navigation using browser_press_key with Tab, Enter, and Escape
  • You MUST use browser_evaluate to check heading structure, form labels, and ARIA attributes
  • You MUST visually assess color contrast on primary text and UI elements
  • If you cannot perform these actions, explicitly state that the Playwright MCP is not available and cannot proceed

Standards Reference

WCAG 2.2 Level AA — the legal and industry standard for web accessibility.

Key principles (POUR):

  • Perceivable — information must be presentable to all users (alt text, captions, contrast)
  • Operable — all functionality must be keyboard accessible (navigation, forms, modals)
  • Understandable — content must be readable and predictable
  • Robust — content must work with assistive technologies (correct ARIA, semantic HTML)

Environment Awareness

The site may be running in a non-production environment (local, development, or staging). The environment may be specified explicitly by the user or inferred from the URL (e.g., .test/.local domains, staging.* subdomains).

  • Local / Development: Accessibility issues injected by dev tooling (admin bars, debug bars, Query Monitor panels) are expected and should not be flagged. Still flag all genuine accessibility issues — heading hierarchy, missing alt text, contrast, keyboard navigation, and ARIA problems exist regardless of environment.
  • Staging: Should mirror production. Flag everything, including issues from debug tools that shouldn't be present.
  • Production: Flag everything.

If you detect signs of a non-production environment that wasn't explicitly specified, note it in the report and apply the guidance above.


MANDATORY SUCCESS CRITERIA — Complete Before Proceeding

  • ✅ Visit at least 4-6 different pages across the site
  • ✅ Run browser_snapshot on every visited page
  • ✅ Extract heading hierarchy on every visited page
  • ✅ Check all images for alt text on every visited page
  • ✅ Check all forms for label associations on any page with forms
  • ✅ Test keyboard navigation (Tab, Enter, Escape) on at least 2-3 pages
  • ✅ Verify focus indicators are visible on all tested pages
  • ✅ Run contrast extraction script on every visited page and report all failures
  • ✅ Measure interactive target sizes (24×24px minimum) on every visited page
  • ✅ Verify focused elements are not fully obscured by sticky/overlapping content during keyboard testing
  • ✅ Run the conditional tests (dragging, authentication, consistent help, redundant entry) on any page where the relevant feature is present
  • ✅ Document all visited pages in the JSON visitedPages array

If you skip any of these steps, the test is incomplete and will not be accepted. (Conditional tests are exempt when the relevant feature is absent — note them as not applicable.)


Testing Workflow Overview

Phase 1: Initial Setup & Structural Assessment

  1. Launch browser at desktop (1920x1080)
  2. Navigate to homepage
  3. Take accessibility tree snapshot
  4. Assess heading structure and landmark regions

Phase 2: Multi-Page Accessibility Testing

  1. Test each page for all criteria in Section 2
  2. Use accessibility tree + JavaScript evaluation + visual inspection

Phase 3: Keyboard Navigation Testing

  1. Tab through 2-3 key pages from start to finish
  2. Test interactive elements: menus, modals, forms

Phase 4: Cross-Page Analysis

  1. Identify patterns vs. page-specific issues
  2. Confirm consistency of navigation and focus across all pages

Phase 5: Data Collection & Reporting

  1. Compile into reports/data/qa-report-accessibility.json
  2. Run report generation script if available

SECTION 1: Initial Setup & Homepage Assessment

1.1 Browser Setup

  • Launch browser at desktop (1920x1080)
  • Navigate to homepage
  • Wait for full page load

1.2 Accessibility Tree Snapshot

Run browser_snapshot immediately after page load. This is your most powerful tool — it reveals:

  • The accessible name of every element
  • Whether images have alt text
  • Whether buttons and links have labels
  • The role of each element (button, link, heading, img, etc.)
  • Form inputs and their associated labels
  • ARIA attributes

What to look for:

  • Images listed without an accessible name → missing alt text
  • Buttons with no label → missing accessible name
  • Links with generic text ("click here", "read more") → not descriptive out of context
  • Form inputs without associated labels

1.3 Heading Hierarchy

Extract heading structure using:

Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).map(h => ({
  tag: h.tagName.toLowerCase(),
  text: h.innerText.trim().substring(0, 80)
}))

Valid hierarchy rules:

  • ✅ Exactly one H1 per page
  • ✅ No skipped levels (H1 → H2 → H3, never H1 → H3)
  • ✅ H1 describes the page's main topic
  • ✅ Heading levels reflect content structure, not visual styling
  • ❌ Multiple H1s on a single page
  • ❌ Skipped levels (e.g. H2 directly followed by H4)
  • ❌ Missing H1 entirely

1.4 Landmark Regions

Check for semantic landmarks:

['header', 'nav', 'main', 'footer', 'aside'].map(tag => ({
  tag,
  count: document.querySelectorAll(tag).length
})).filter(r => r.count > 0)

Expected on most pages:

  • <header> — site header
  • <nav> — navigation
  • <main> — main content area
  • <footer> — site footer

Flag if missing:

  • No <main> element → screen readers cannot skip to main content (high priority)
  • Navigation not wrapped in <nav> → reduces keyboard efficiency (medium)

SECTION 2: Per-Page Accessibility Testing

For each page visited, perform all tests below. Repeat for at least 4-6 pages.

A. Heading Hierarchy

Run the JavaScript from Section 1.3 on each page.

  • ✅ Exactly one H1 per page
  • ✅ No skipped heading levels
  • ✅ Headings describe the content of their section
  • ❌ H1 missing → report as high priority
  • ❌ Heading levels skipped → report as medium priority
  • ❌ Multiple H1 tags → report as medium priority

B. Images & Alt Text

Run browser_snapshot and check image entries, or evaluate directly:

Array.from(document.querySelectorAll('img')).map(img => ({
  src: img.src.split('/').pop().substring(0, 60),
  alt: img.alt,
  hasAlt: img.hasAttribute('alt'),
  isDecorative: img.alt === '',
  isLazy: img.loading === 'lazy'
}))

Rules:

  • ✅ Meaningful images have descriptive alt text (not just filenames)
  • ✅ Decorative images have alt="" (empty string, not missing)
  • ✅ Images used as links describe the destination in alt text
  • alt attribute completely absent → report as high priority
  • ❌ Alt text that is just a filename (e.g. img-001.jpg) → report as medium
  • ❌ Alt text that says "image of..." or "photo of..." (redundant) → report as low

Note: An image with alt="" is intentionally decorative — this is correct and should not be flagged.

C. Color Contrast

WCAG 2.2 AA contrast thresholds:

  • Normal text (< 18pt regular or < 14pt bold): 4.5:1 minimum
  • Large text (≥ 18pt regular or ≥ 14pt bold): 3:1 minimum
  • UI components (buttons, form borders, icons): 3:1 minimum

Elements to check:

  • Body/paragraph text against page background
  • Heading text against background
  • Navigation links (active and inactive states)
  • Button text against button background
  • Footer text (frequently too light)
  • Text overlaid on hero images or gradients

Programmatic contrast extraction

You MUST run the following script on every page to extract computed colors from key UI elements. This catches issues that visual assessment misses — especially elements with transparent or semi-transparent backgrounds.

Important: resolving transparent backgrounds. Many elements use rgba() or transparent backgrounds, meaning the visible background is actually inherited from an ancestor. The script below walks up the DOM to find the first opaque background and composites any semi-transparent layers on top of it. You must do the same if you manually check any element's contrast — never treat a transparent background as the final color.

(() => {
  // Parse an rgb/rgba string into {r, g, b, a}
  function parseColor(str) {
    const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
    if (!m) return null;
    return { r: +m[1], g: +m[2], b: +m[3], a: m[4] !== undefined ? +m[4] : 1 };
  }

  // Composite a semi-transparent foreground over an opaque background
  function composite(fg, bg) {
    return {
      r: Math.round(fg.r * fg.a + bg.r * (1 - fg.a)),
      g: Math.round(fg.g * fg.a + bg.g * (1 - fg.a)),
      b: Math.round(fg.b * fg.a + bg.b * (1 - fg.a)),
      a: 1
    };
  }

  // Walk up the DOM to resolve the effective background color
  function resolveBackground(el) {
    let layers = [];
    let current = el;
    while (current) {
      const bg = parseColor(window.getComputedStyle(current).backgroundColor);
      if (bg) {
        layers.push(bg);
        if (bg.a === 1) break; // found an opaque layer, stop
      }
      current = current.parentElement;
    }
    // If no opaque layer found, assume white
    let result = { r: 255, g: 255, b: 255, a: 1 };
    // Composite from bottom (most distant ancestor) to top (element itself)
    for (let i = layers.length - 1; i >= 0; i--) {
      result = composite(layers[i], result);
    }
    return result;
  }

  // Relative luminance per WCAG 2.x
  function luminance(c) {
    const [rs, gs, bs] = [c.r, c.g, c.b].map(v => {
      v = v / 255;
      return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
  }

  // Contrast ratio
  function contrastRatio(c1, c2) {
    const l1 = luminance(c1), l2 = luminance(c2);
    const lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
    return +((lighter + 0.05) / (darker + 0.05)).toFixed(2);
  }

  // Collect elements to check
  const selectors = 'a, button, p, h1, h2, h3, h4, h5, h6, span, li, td, th, label, input, select, textarea';
  const seen = new Set();
  const results = [];

  document.querySelectorAll(selectors).forEach(el => {
    const text = el.textContent?.trim().substring(0, 40);
    if (!text || seen.has(el)) return;
    seen.add(el);

    const styles = window.getComputedStyle(el);
    const textColor = parseColor(styles.color);
    const effectiveBg = resolveBackground(el);
    if (!textColor || !effectiveBg) return;

    const ratio = contrastRatio(textColor, effectiveBg);
    const fontSize = parseFloat(styles.fontSize);
    const fontWeight = parseInt(styles.fontWeight) || 400;
    const isLarge = fontSize >= 24 || (fontSize >= 18.66 && fontWeight >= 700);
    const threshold = isLarge ? 3 : 4.5;

    if (ratio < threshold) {
      results.push({
        tag: el.tagName.toLowerCase(),
        text: text,
        textColor: `rgb(${textColor.r},${textColor.g},${textColor.b})`,
        effectiveBg: `rgb(${effectiveBg.r},${effectiveBg.g},${effectiveBg.b})`,
        ratio: ratio,
        threshold: threshold,
        fontSize: fontSize + 'px',
        fontWeight: fontWeight,
        isLarge: isLarge
      });
    }
  });

  return results.length ? results : 'All checked elements meet contrast thresholds';
})()

Any element returned by this script is a contrast failure — report it. Also visually assess text overlaid on images or gradients, which the script cannot measure.

Flag if:

  • Any element returned by the contrast script → report with the actual ratio and threshold
  • Footer text appears muted → common failure point
  • Text on images lacks sufficient contrast (check visually)

D. Keyboard Navigation

Test keyboard accessibility by tabbing through the page.

How to test:

  1. Click once on the page to give it focus
  2. Use browser_press_key with "Tab" to move forward through focusable elements
  3. Use browser_press_key with "Shift+Tab" to move backward
  4. After each Tab, check the focused element:
document.activeElement.tagName + ': ' +
  (document.activeElement.textContent?.trim().substring(0, 60) ||
   document.activeElement.getAttribute('aria-label') ||
   document.activeElement.getAttribute('placeholder') ||
   '(no label)')

What to verify:

  • ✅ All links, buttons, and form fields are reachable by Tab
  • ✅ Tab order follows visual reading order (top → bottom, left → right)
  • ✅ Focus indicator is visible at every step (outline, highlight, or other indicator)
  • ✅ No element traps focus (Tab always eventually moves on)
  • ✅ Escape closes any open modal, dropdown, or off-canvas menu
  • ✅ Skip navigation link appears on first Tab press (if implemented)

Flag if:

  • Any interactive element is not reachable by Tab → critical
  • Focus indicator is invisible (CSS outline: none with no replacement) → high
  • Tab order doesn't match visual order → medium
  • Focus never escapes a component (keyboard trap) → critical

E. Forms & Input Labels

On any page with forms, run:

Array.from(document.querySelectorAll('input:not([type="hidden"]), select, textarea'))
  .map(input => {
    const id = input.id;
    const label = id ? document.querySelector(`label[for="${id}"]`) : null;
    const ariaLabel = input.getAttribute('aria-label');
    const ariaLabelledBy = input.getAttribute('aria-labelledby');
    const placeholder = input.getAttribute('placeholder');
    return {
      type: input.type || input.tagName.toLowerCase(),
      id: id || '(no id)',
      hasLabel: !!label,
      hasAriaLabel: !!ariaLabel,
      hasAriaLabelledBy: !!ariaLabelledBy,
      placeholder: placeholder || null,
      accessible: !!(label || ariaLabel || ariaLabelledBy)
    };
  })

Requirements:

  • ✅ Every input has an associated <label>, aria-label, or aria-labelledby
  • placeholder alone is NOT a sufficient label (disappears on typing)
  • ✅ Required fields marked with required attribute and visually indicated
  • ✅ Submit button has accessible text

Flag if:

  • Input with no label of any kind → critical
  • Input that only has placeholder as its identification → high
  • Required fields not visually marked → medium

F. ARIA & Semantic Checks

({
  skipLink: !!document.querySelector('a[href="#main"], a[href="#content"], a[href="#maincontent"], .skip-link, [class*="skip"]'),
  mainLandmark: !!document.querySelector('main, [role="main"]'),
  navCount: document.querySelectorAll('nav, [role="navigation"]').length,
  buttonsWithoutText: Array.from(document.querySelectorAll('button')).filter(b =>
    !b.textContent?.trim() &&
    !b.getAttribute('aria-label') &&
    !b.getAttribute('aria-labelledby')
  ).length,
  ariaHiddenOnFocusable: Array.from(document.querySelectorAll('[aria-hidden="true"]'))
    .filter(el => el.querySelector('a, button, input, select, textarea') ||
      ['A','BUTTON','INPUT','SELECT','TEXTAREA'].includes(el.tagName)).length
})

Flag if:

  • No skip link → high priority (screen readers and keyboard users must tab through entire nav on every page)
  • No <main> or role="main" → high priority
  • Buttons without accessible text → high priority (icon-only buttons need aria-label)
  • aria-hidden="true" on or containing interactive elements → critical

G. Motion & Animation

Visually assess:

  • ✅ Animations are not distracting or excessively rapid
  • ✅ No content flashes more than 3 times per second (seizure risk)
  • ✅ Auto-playing video or carousels can be paused
  • ✅ Carousels/sliders provide sufficient time to read before advancing

Check if prefers-reduced-motion is respected:

window.matchMedia('(prefers-reduced-motion: reduce)').matches

If the site has significant animation, note whether this media query is handled.

H. Target Size (Minimum)

WCAG 2.2 AA (2.5.8) requires pointer targets to be at least 24×24 CSS pixels, unless an exception applies. Measure interactive elements:

Array.from(document.querySelectorAll('a, button, input:not([type="hidden"]), select, textarea, summary, [role="button"], [role="link"], [tabindex], [contenteditable]:not([contenteditable="false"]), [onclick]'))
  .map(el => {
    const r = el.getBoundingClientRect();
    return {
      tag: el.tagName.toLowerCase(),
      text: (el.textContent || el.getAttribute('aria-label') || '').trim().substring(0, 40),
      width: r.width,
      height: r.height
    };
  })
  .filter(el => el.width > 0 && el.height > 0 && (el.width < 24 || el.height < 24))
  .map(el => ({ ...el, width: Math.round(el.width), height: Math.round(el.height) }));

Exceptions — do NOT flag if any apply:

  • Spacing: a 24px-diameter circle centred on the target does not overlap any adjacent target's circle (enough clear space around it)
  • Equivalent: the same function is provided by another control on the same page that does meet 24×24 (e.g. a small icon toggle that duplicates a full-size text link)
  • Inline: the target is a link inside a sentence or block of text
  • Essential: the small size is legally required or essential to the information conveyed (e.g. a pin on a map at a precise location)
  • User-agent controlled: the size is the browser default and not modified by the author's CSS

Flag if an interactive target is under 24×24 CSS px and no exception applies → report as target-too-small. Common offenders: icon-only social links, close (×) buttons, tightly packed footer links, pagination numbers.


SECTION 3: Keyboard Navigation Deep Dive

For 2-3 key pages (homepage required, plus at least one content-heavy page):

Full Tab Walk-through

  1. Click the browser page to give it focus
  2. Press Tab — the first element focused should ideally be a skip navigation link
  3. Continue pressing Tab, noting each focused element and whether focus is visible
  4. Continue until you've reached the footer
  5. Document the total number of focusable elements

What to record:

  • Were all interactive elements reachable?
  • Was focus indicator visible throughout?
  • Was tab order logical?
  • Any keyboard traps?
  • Did a skip link appear on first Tab?

Navigation Menu Testing

If the site has dropdown navigation:

  1. Tab to a navigation item that has a dropdown
  2. Press Enter or Space to open it
  3. Verify dropdown opens and focus moves into it
  4. Tab through the dropdown items
  5. Press Escape — verify dropdown closes and focus returns to trigger
  6. Note if arrow key navigation is implemented (good practice but not required at AA)

Skip Navigation Testing

  1. From a fresh page load, press Tab once
  2. A skip link ("Skip to content" or similar) should appear
  3. Press Enter — verify focus jumps to the main content area, bypassing navigation

If no skip link exists: flag as high priority.

Focus Not Obscured

As you Tab through each page — especially with sticky or fixed headers, footers, or cookie banners present:

  • ✅ When an element receives focus, at least part of it remains visible — it is not entirely hidden behind a sticky header, sticky footer, cookie bar, or other overlapping layer.
  • ❌ A focused element is completely covered by fixed/overlapping content → report as focus-obscured (2.4.11 Focus Not Obscured (Minimum)).

This commonly fails when a focused element scrolls underneath a sticky header, or when a fixed bar overlaps an in-page anchor target.


SECTION 3.5: Conditional Tests — Run Only If the Feature Is Present

These criteria apply only when the site has the relevant feature. If the feature is absent, note it as not applicable rather than flagging anything — these tests do not block report generation when they don't apply.

Dragging Movements (2.5.7)

Applies if the site has any interaction that requires dragging — range sliders, drag-and-drop, draggable maps, reorderable lists, image-comparison sliders.

  • ✅ Every drag operation has a single-pointer alternative (tap/click, arrow buttons, +/− controls) that achieves the same result without dragging.
  • ❌ A function can only be operated by dragging → report as drag-no-alternative (2.5.7 Dragging Movements).

Accessible Authentication (3.3.8)

Applies if the site has a login, account, or other authentication step.

  • ✅ Authentication does not require a cognitive function test (memorising or transcribing a value, solving a puzzle, identifying images) unless an alternative method or assistance mechanism exists.
  • ✅ The password field allows paste, and password managers / browser autofill are not blocked.
  • ❌ Auth relies on a cognitive function test with no accessible alternative, or blocks paste / password managers → report as auth-cognitive-test (3.3.8 Accessible Authentication (Minimum)).

Standard username + password (with paste allowed) passes. A CAPTCHA requiring a puzzle solve with no alternative fails.

Consistent Help (3.2.6)

Applies if the site offers a help mechanism that appears across multiple pages — a contact link, help link, phone number, chat widget, or self-help/FAQ link.

  • ✅ The help mechanism appears in the same relative order/location across the pages that include it (e.g. always in the header, or always bottom-right).
  • ❌ Help is placed inconsistently across pages → report as inconsistent-help (3.2.6 Consistent Help).

Redundant Entry (3.3.7)

Applies if the site has a multi-step process — checkout, multi-page form, multi-stage signup.

  • ✅ Information already entered earlier in the same process is auto-populated or available to select, not required to be re-entered (except where re-entry is essential, e.g. confirming a password).
  • ❌ The user must manually re-enter information they already provided in the same process → report as redundant-entry (3.3.7 Redundant Entry).

SECTION 4: Cross-Page Analysis

4.1 Consistency Check

After testing all pages, confirm:

  • ✅ Heading hierarchy follows the same patterns across pages
  • ✅ Focus indicators are visible on all pages (a CSS issue will affect all pages globally)
  • ✅ Alt text is consistently present across all pages
  • ✅ Keyboard navigation works on all pages, not just homepage
  • ✅ Footer links are keyboard accessible on all pages

4.2 Issue Patterns

  • Are missing alt text issues site-wide or on specific pages? (site-wide = theme-level issue)
  • Are heading hierarchy issues consistent or page-specific?
  • Are contrast issues confined to specific components (hero section, footer)?
  • Are keyboard/focus issues global (CSS file) or page-specific?

MANDATORY TESTING CHECKLIST

Pages Tested

  • Homepage: _____________________
  • Page 2: _____________________
  • Page 3: _____________________
  • Page 4: _____________________
  • Page 5 (optional): _____________________
  • Page 6 (optional): _____________________

Minimum pages: 4. You have tested _____ pages.

Tests Completed Per Page

  • browser_snapshot taken on all pages
  • Heading hierarchy extracted on all pages
  • Images checked for alt text on all pages
  • Forms checked for labels on all pages with forms
  • Contrast extraction script run on all pages
  • Landmark regions checked on all pages
  • Interactive target sizes measured (24×24px) on all pages

Keyboard Navigation

  • Full Tab walk-through completed on at least 2 pages
  • Focus indicators confirmed visible
  • No keyboard traps encountered
  • Skip navigation link tested (or absence documented)
  • Navigation menu keyboard behavior tested
  • Focus not obscured by sticky/overlapping content confirmed

Conditional Tests (mark N/A if the feature is absent)

  • Dragging movements: single-pointer alternative verified
  • Accessible authentication tested on login/auth flows
  • Consistent help placement verified across pages
  • Redundant entry checked in multi-step flows

Ready for JSON Report

  • All pages listed in visitedPages array
  • All issues categorised with type, severity, and affected element
  • WCAG criterion referenced on each issue where applicable

If any item is unchecked, do NOT generate the JSON report. Return to Section 2 and complete the missing tests.


SECTION 5: Data Collection

Populate reports/data/qa-report-accessibility.json:

{
  "url": "https://example.com",
  "websiteName": "Example",
  "timestamp": "YYYY-MM-DDTHH:MM:SSZ",
  "wcag_standard": "WCAG 2.2 Level AA",
  "visitedPages": [
    "https://example.com/",
    "https://example.com/about/",
    "https://example.com/services/",
    "https://example.com/contact/"
  ],
  "mobile": {
    "viewport": "375x812",
    "title": "Page Title",
    "url": "https://example.com",
    "a11y": [
      {"type": "missing-alt", "element": "Hero banner image (hero.jpg)", "severity": "high"},
      {"type": "missing-label", "element": "Email input in newsletter form", "severity": "critical"},
      {"type": "button-no-text", "element": "Mobile menu toggle button", "severity": "high"}
    ],
    "focusableElements": 38
  },
  "desktop": {
    "viewport": "1920x1080",
    "title": "Page Title",
    "url": "https://example.com",
    "a11y": [
      {"type": "missing-alt", "element": "Hero banner image (hero.jpg)", "severity": "high"},
      {"type": "heading-skip", "element": "H1 followed directly by H3 in Services section", "severity": "medium"},
      {"type": "no-focus-indicator", "element": "Primary CTA button", "severity": "high"},
      {"type": "missing-skip-link", "element": "No skip navigation link on page", "severity": "high"},
      {"type": "low-contrast", "element": "Footer copyright text (#999 on #fff)", "severity": "medium"}
    ],
    "focusableElements": 54
  },
  "issues": {
    "critical": [
      {
        "category": "Accessibility",
        "issue": "Brief description of the issue",
        "impact": "How this affects users with disabilities",
        "device": "mobile|desktop|both",
        "pages": ["https://example.com/contact/"],
        "wcag_criterion": "1.3.1 Info and Relationships",
        "screenshots": ["screenshots/example-finding.png"]
      }
    ],
    "high": [],
    "medium": [],
    "low": []
  }
}

screenshots field (optional but strongly encouraged for visual a11y findings): Attach screenshots for findings where a visual is helpful — low-contrast text examples, missing focus indicators, touch targets that look too small, broken keyboard-only navigation flows. The path is relative to the reports/ directory (e.g., screenshots/contact-form-focus-state.png for a file saved at reports/screenshots/contact-form-focus-state.png). For non-visual findings (missing alt text on images that aren't themselves the problem, ARIA misuse, etc.), skip the field.

A11y Issue Types

Use these standardised type values in the a11y array:

Type Description
missing-alt Image missing alt attribute, or non-empty alt when image is decorative
missing-label Form input has no associated label
button-no-text Button has no accessible name (no text, aria-label, or aria-labelledby)
heading-skip Heading levels are skipped (e.g. H1 → H3)
missing-h1 Page has no H1 tag
multiple-h1 Page has more than one H1 tag
low-contrast Text/background contrast ratio below WCAG AA threshold
no-focus-indicator Interactive element has no visible focus indicator
not-keyboard-accessible Interactive element cannot be reached or operated by keyboard
keyboard-trap Keyboard focus cannot escape an area
missing-skip-link Page has no skip navigation link
missing-landmark Page missing expected landmark region (main, nav, etc.)
aria-hidden-interactive aria-hidden applied to a focusable element
placeholder-only-label Form input relies solely on placeholder for identification
target-too-small Interactive target smaller than 24×24 CSS px with no qualifying exception
focus-obscured Focused element is fully hidden by sticky or overlapping content
drag-no-alternative A drag operation has no single-pointer alternative
auth-cognitive-test Authentication requires a cognitive function test with no accessible alternative
inconsistent-help Help mechanism not in a consistent location across pages that include it
redundant-entry User must re-enter information already provided earlier in the same process

Issue Priority Guide

  • Critical — completely blocks a screen reader or keyboard user (missing form labels, keyboard traps, aria-hidden on interactive elements, authentication that can't be completed without a cognitive function test)
  • High — significantly impacts the experience (missing alt on informational images, missing skip link, no focus indicator, buttons without text, focused element fully obscured, a function operable only by dragging)
  • Medium — reduces quality but workarounds exist (low contrast, skipped heading levels, generic link text, targets below 24×24px, inconsistent help placement, redundant entry in multi-step flows)
  • Low — best practice violations with minor impact (decorative images missing empty alt, minor ARIA improvements)

WCAG 2.2 Criteria Reference

A finding's wcag_criterion is determined by its type — look it up in this table, do not generate it from memory. Cite only a criterion that appears below, using the exact title shown. If a finding does not map to any row, leave wcag_criterion blank rather than inventing or approximating a number.

type WCAG 2.2 Criterion
missing-alt 1.1.1 Non-text Content
missing-label 1.3.1 Info and Relationships
placeholder-only-label 3.3.2 Labels or Instructions
button-no-text 4.1.2 Name, Role, Value
aria-hidden-interactive 4.1.2 Name, Role, Value
heading-skip 1.3.1 Info and Relationships
missing-h1 1.3.1 Info and Relationships
multiple-h1 1.3.1 Info and Relationships
missing-landmark 1.3.1 Info and Relationships
low-contrast 1.4.3 Contrast (Minimum) for text; 1.4.11 Non-text Contrast for UI components, icons, and graphical objects
no-focus-indicator 2.4.7 Focus Visible
focus-obscured 2.4.11 Focus Not Obscured (Minimum)
not-keyboard-accessible 2.1.1 Keyboard
keyboard-trap 2.1.2 No Keyboard Trap
missing-skip-link 2.4.1 Bypass Blocks
target-too-small 2.5.8 Target Size (Minimum)
drag-no-alternative 2.5.7 Dragging Movements
auth-cognitive-test 3.3.8 Accessible Authentication (Minimum)
inconsistent-help 3.2.6 Consistent Help
redundant-entry 3.3.7 Redundant Entry

SECTION 6: Report Generation

Once reports/data/qa-report-accessibility.json is populated:

scripts/run-qa-report.sh reports/data/qa-report-accessibility.json

To merge with functional and performance reports:

scripts/merge-qa-reports.sh reports/data/qa-report-functional.json reports/data/qa-report-performance.json reports/data/qa-report-accessibility.json

Accessibility Testing Notes

Using browser_snapshot Effectively

The accessibility tree snapshot is your most powerful tool. Run it on every page before anything else. It reveals at a glance:

  • Which images lack alt text (they appear without an accessible name)
  • Which buttons have no label
  • Whether form inputs are associated with labels
  • The semantic role of every element

If the snapshot output is very long, focus first on: images, buttons, inputs, and headings.

Color Contrast Notes

The contrast extraction script in Section 2C catches most text-on-background failures, including elements with transparent or semi-transparent backgrounds. However, it cannot measure:

  • Text overlaid on background images or gradients (no single background color to extract)
  • Text rendered inside <canvas> or <svg> elements
  • Contrast of non-text UI components like icon-only indicators

For these cases, visually assess contrast and flag anything that appears marginal. When in doubt, extract the element's colors manually using browser_evaluate and calculate the ratio.

WordPress-Specific Patterns

  • Contact Form 7 / WPForms / Gravity Forms — generally output correct label markup, but custom CSS themes can hide focus indicators; always test these forms by keyboard
  • Navigation menus — WordPress themes vary widely in keyboard support; always test dropdowns by keyboard, not just visually
  • Gutenberg blocks — core blocks generally have good semantics; custom third-party blocks may not
  • Sliders and carousels — common source of auto-play issues, keyboard traps, and missing pause controls
  • Cookie consent banners — must be keyboard dismissible; if they trap focus, that is a critical issue
  • Lightboxes and modals — frequently implemented without proper focus management; always test by keyboard

What Passes vs. Fails Keyboard Testing

Passes: Every interactive element is reachable by Tab, focus is always visible, Escape closes modals, focus returns to trigger after modal closes.

Fails: Any interactive element not reachable by Tab, focus disappears entirely, pressing Tab infinitely cycles within one component without escape.

Install via CLI
npx skills add https://github.com/a8cteam51/kosh --skill a11y
Repository Details
star Stars 6
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator