Skip to main content

Developer Kit

Accessibility Auditor

Scans UI components for WCAG 2.2 AA violations including ARIA gaps, keyboard navigation issues, contrast failures, and screen reader compatibility. Useful for legal compliance and inclusive product development. Frontend engineers shipping UI, product teams preparing for enterprise procurement checklists, startups responding to accessibility-clause contract requirements, engineers working on public-sector software. AI-generated UI components consistently ship without the accessibility scaffolding that experienced frontend engineers apply reflexively: ARIA labels on icon-only buttons, focus management in modals, semantic landmarks, color contrast meeting AA ratios, keyboard handlers on interactive elements. A pre-ship auditor catches the high-signal violations before they reach production, which is when remediation is cheap.

Nexus CertifiedClaude CodeCodexOpenClaw
accessibilitywcaga11yfrontendcompliance

One-Time Purchase

$19.99

Sample Output

Accessibility Audit — src/components/ProductFilterSidebar.tsx

Framework: React 19 + Radix UI
Target WCAG: 2.2 AA
Audit Date: 2025-06-11
Auditor: ClearPoint Nexus — Accessibility Auditor Skill


Audit Summary

| Metric | Value | |---|---| | Components scanned | 7 (FilterSidebar, FilterGroup, PriceRangeSlider, ColorSwatchGroup, ApplyButton, ClearFiltersLink, CollapsibleSection) | | Total findings | 11 | | Critical | 2 | | Serious | 4 | | Moderate | 3 | | Minor | 2 | | Manual-review flags | 4 |

Findings by WCAG Principle

| Principle | Findings | |---|---| | Perceivable | 4 | | Operable | 4 | | Understandable | 2 | | Robust | 1 |

⚠️ Scope notice: Static analysis can confirm structural and attribute-level violations. Dynamic behaviors — focus-trap correctness when filters apply, live-region announcement timing, and screen reader virtual-cursor interaction — require manual testing with NVDA/JAWS/VoiceOver. Screen reader testing with actual users is the final quality gate for any WCAG conformance claim.


Findings


F-01 · CRITICAL — Color Contrast Failure on Disabled Filter Count

WCAG Criterion: 1.4.3 Contrast (Minimum) — Level AA
Location: FilterGroup.tsx line 42 — <span className="filter-count-disabled">
Type: 🔴 Automatic finding

Narrative: The disabled-state filter count badge renders #9CA3AF (gray-400) text on a #F9FAFB (gray-50) background. Measured contrast ratio is 2.1:1, well below the 4.5:1 minimum required for normal text at this size (13px/0.8rem, non-bold).

Measured ratio: 2.1:1 (required: 4.5:1)

Remediation:

/* Before */
.filter-count-disabled {
  color: #9CA3AF; /* gray-400 */
  background-color: #F9FAFB; /* gray-50 */
}

/* After — passes at 5.3:1 */
.filter-count-disabled {
  color: #6B7280; /* gray-500 */
  background-color: #F9FAFB; /* gray-50 */
}

F-02 · CRITICAL — Price Range Slider Inaccessible to Keyboard and Screen Readers

WCAG Criterion: 4.1.2 Name, Role, Value — Level A | 2.1.1 Keyboard — Level A
Location: PriceRangeSlider.tsx lines 18–61 — custom dual-handle <div> slider
Type: 🔴 Automatic finding

Narrative: Both range handles are implemented as unstyled <div> elements with mouse-only drag handlers. Neither handle has role="slider", aria-valuenow, aria-valuemin, aria-valuemax, or aria-label. No tabIndex is set, making them unreachable by keyboard. A screen reader user has no mechanism to discover or adjust the price range.

Remediation: Replace custom implementation with native <input type="range"> styled to match the design, or — if the dual-handle behavior must be custom — apply the ARIA slider pattern:

// Preferred: native inputs styled with CSS (no ARIA needed)
<div className="price-range-track">
  <input
    type="range"
    id="price-min"
    min={0}
    max={500}
    value={minPrice}
    onChange={(e) => setMinPrice(Number(e.target.value))}
    className="range-thumb range-thumb--min"
    aria-label="Minimum price"
  />
  <input
    type="range"
    id="price-max"
    min={0}
    max={500}
    value={maxPrice}
    onChange={(e) => setMaxPrice(Number(e.target.value))}
    className="range-thumb range-thumb--max"
    aria-label="Maximum price"
  />
</div>

First rule of ARIA: native <input type="range"> is preferred over role="slider" on a <div>. Use the custom ARIA pattern only if native inputs cannot meet the interaction design requirement after CSS styling.


F-03 · SERIOUS — Color Swatches Convey State by Color Alone

WCAG Criterion: 1.4.1 Use of Color — Level A | 4.1.2 Name, Role, Value — Level A
Location: ColorSwatchGroup.tsx lines 29–55
Type: 🔴 Automatic finding

Narrative: Selected and unavailable swatch states are communicated exclusively through background color (ring shadow and opacity). No programmatic state is exposed. A keyboard or screen reader user cannot determine which color is selected or which are out of stock.

Remediation:

// Before
<button className={isSelected ? 'swatch swatch--selected' : 'swatch'}>
  <span style={{ background: color.hex }} />
</button>

// After
<button
  className={isSelected ? 'swatch swatch--selected' : 'swatch'}
  aria-pressed={isSelected}
  aria-label={`${color.name}${isUnavailable ? ', unavailable' : ''}`}
  disabled={isUnavailable}
>
  <span style={{ background: color.hex }} aria-hidden="true" />
</button>

F-04 · SERIOUS — CollapsibleSection Toggle Missing Accessible Name and State

WCAG Criterion: 4.1.2 Name, Role, Value — Level A | 1.3.1 Info and Relationships — Level A
Location: CollapsibleSection.tsx line 14 — toggle <button>
Type: 🔴 Automatic finding

Narrative: The collapse/expand button contains only an SVG chevron icon with no text content, no aria-label, and no aria-expanded attribute. Screen readers will announce "button" with no name, and the collapsed/expanded state is invisible to assistive technology.

Remediation:

// Before
<button className="collapsible-toggle" onClick={toggle}>
  <ChevronIcon className={isOpen ? 'rotate-180' : ''} />
</button>

// After
<button
  className="collapsible-toggle"
  onClick={toggle}
  aria-expanded={isOpen}
  aria-controls={`filter-section-${sectionId}`}
  aria-label={`${sectionLabel} filter group`}
>
  <ChevronIcon className={isOpen ? 'rotate-180' : ''} aria-hidden="true" />
</button>

// Panel
<div id={`filter-section-${sectionId}`}>
  {children}
</div>

F-05 · SERIOUS — "Clear Filters" Implemented as <a> With No href

WCAG Criterion: 2.1.1 Keyboard — Level A | 4.1.2 Name, Role, Value — Level A
Location: ClearFiltersLink.tsx line 8
Type: 🔴 Automatic finding

Narrative: <a> without an href is not keyboard focusable by default in most browsers and has an ambiguous role (link vs. button). Because this control triggers a JavaScript action rather than navigation, a <button> is the correct semantic element.

Remediation:

// Before
<a className="clear-filters" onClick={clearAllFilters}>
  Clear all filters
</a>

// After — correct semantic element, no ARIA needed
<button
  type="button"
  className="clear-filters"
  onClick={clearAllFilters}
>
  Clear all filters
</button>

F-06 · SERIOUS — Filter Group Fieldset/Legend Missing

WCAG Criterion: 1.3.1 Info and Relationships — Level A | 2.4.6 Headings and Labels — Level AA
Location: FilterGroup.tsx lines 10–38 — checkbox groups for Size, Category
Type: 🔴 Automatic finding

Narrative: Each checkbox cluster is wrapped in a <div> with a sibling <p> label (e.g., "Size"). The visual label is not programmatically associated with the group. Screen readers will announce each checkbox in isolation without the group context ("Small" rather than "Size: Small").

Remediation:

// Before
<div className="filter-group">
  <p className="filter-group-label">Size</p>
  {sizes.map(size => <Checkbox key={size} label={size} />)}
</div>

// After — native fieldset/legend; no ARIA required
<fieldset className="filter-group">
  <legend className="filter-group-label">Size</legend>
  {sizes.map(size => <Checkbox key={size} label={size} />)}
</fieldset>

F-07 · MODERATE — Focus Indicator Suppressed Globally

WCAG Criterion: 2.4.11 Focus Appearance — Level AA (WCAG 2.2)
Location: globals.css line 3
Type: 🔴 Automatic finding

Narrative: *:focus { outline: none; } removes all native focus indicators site-wide. Component-level :focus-visible styles exist on ApplyButton but not on the checkbox inputs, swatches, or collapsible toggles, leaving those controls invisible to keyboard users when focused.

Remediation:

/* Remove the global suppression */
/* DELETE: *:focus { outline: none; } */

/* Replace with a consistent :focus-visible style */
:focus-visible {
  outline: 3px solid #2563EB; /* blue-600, 3:1 contrast against white */
  outline-offset: 2px;
  border-radius: 2px;
}

/* Suppress only where a custom focus style is explicitly provided */
.swatch:focus-visible,
.collapsible-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px #2563EB;
}

F-08 · MODERATE — Apply Button Lacks Live Region for Result Feedback

WCAG Criterion: 4.1.3 Status Messages — Level AA
Location: ApplyButton.tsx lines 22–34
Type: 🔴 Automatic finding

Narrative: After filters apply, the product count updates visually ("124 results") but no programmatic status message is announced. Screen reader users receive no feedback that the action completed or how many results remain.

Remediation:

// Add a visually hidden live region to FilterSidebar
<div
  role="status"
  aria-live="polite"
  aria-atomic="true"
  className="sr-only"
>
  {resultsCount !== null ? `${resultsCount} products found` : ''}
</div>

// Update resultsCount in state after filters resolve

F-09 · MODERATE — Checkbox Inputs Use id Collision Risk Pattern

WCAG Criterion: 1.3.1 Info and Relationships — Level A
Location: FilterGroup.tsx line 26 — <input id={label}> inside .map()
Type: 🔴 Automatic finding

Narrative: Checkbox id values are derived directly from the filter label string (e.g., id="Small"). If FilterGroup is rendered more than once on a page (Size and Category both contain labels that could collide), <label for="..."> associations break, and screen readers may announce the wrong label.

Remediation:

// Before
<input type="checkbox" id={label} />
<label htmlFor={label}>{label}</label>

// After — stable, scoped id
const uid = `${sectionId}-${label.toLowerCase().replace(/\s+/g, '-')}`;
<input type="checkbox" id={uid} />
<label htmlFor={uid}>{label}</label>

F-10 · MINOR — SVG Icons Missing aria-hidden

WCAG Criterion: 1.1.1 Non-text Content — Level A
Location: ChevronIcon.tsx, FilterIcon.tsx
Type: 🔴 Automatic finding

Narrative: Decorative SVGs are passed directly without aria-hidden="true". Screen readers may attempt to read internal <path> or <title> nodes, producing noise. Where the icon is decorative (parent button has an accessible name), it must be hidden from the accessibility tree.

Remediation:

// Decorative icon inside a named button
<ChevronIcon aria-hidden="true" focusable="false" />

// If an icon must be meaningful and stands alone, use title + aria-labelledby
<svg role="img" aria-labelledby="filter-icon-title">
  <title id="filter-icon-title">Open filters</title>
  ...
</svg>

F-11 · MINOR — Color Contrast: "Apply Filters" Button Hover State

WCAG Criterion: 1.4.3 Contrast (Minimum) — Level AA
Location: ApplyButton.tsx.apply-btn:hover in ApplyButton.module.css line 18
Type: 🔴 Automatic finding

Measured ratio (hover): 3.8:1 (#FFFFFF on #3B82F6 blue-500) — required: 4.5:1

Remediation:

/* Before */
.apply-btn:hover { background-color: #3B82F6; } /* blue-500, 3.8:1 */

/* After — blue-600 passes at 5.9:1 */
.apply-btn:hover { background-color: #2563EB; } /* blue-600 */

Color Contrast Report

| Element | Text Color | Background | Ratio | Required | Status | |---|---|---|---|---|---| | Filter count (disabled) | #9CA3AF | #F9FAFB | 2.1:1 | 4.5:1 | ❌ FAIL (F-01) | | Checkbox label (default) | #111827 | #FFFFFF | 16.1:1 | 4.5:1 | ✅ PASS | | Apply button (default) | #FFFFFF | #1D4ED8 | 8.6:1 | 4.5:1 | ✅ PASS | | Apply button (hover) | #FFFFFF | #3B82F6 | 3.8:1 | 4.5:1 | ❌ FAIL (F-11) | | Clear filters link | #6B7280 | #FFFFFF | 4.6:1 | 4.5:1 | ✅ PASS | | Section label text | #374151 | #F3F4F6 | 7.2:1 | 4.5:1 | ✅ PASS | | Unavailable swatch label | #9CA3AF | #FFFFFF | 2.5:1 | 4.5:1 | ⚠️ Manual review — text present? |


Keyboard Traversal Report

Inferred from static source. Requires manual verification with keyboard-only navigation.

| Component | Tab Stop | Arrow Key Support | Expected | Issue | |---|---|---|---|---| | CollapsibleSection toggle | ❌ Not reachable (no tabIndex, no button role) | N/A | Yes | F-04 | | `PriceRangeSlider

View full sample →

All sales final. No refunds on digital products.

Includes support for Claude Code, Codex, and OpenClaw in the same license.

What You Get With This Skill

Scans UI components for WCAG 2.2 AA violations including ARIA gaps, keyboard navigation issues, contrast failures, and screen reader compatibility. Useful for legal compliance and inclusive product development.

All ClearPoint Nexus Skills Include

  • Production-ready workflow packaging for three supported platforms.
  • Reusable structure designed for repeatable operator tasks.
  • Clear deliverable format, not just raw prompt output.

Related Skills

Developer Kit
Featured
Code Generation
Generates, reviews, debugs, and executes code in sandboxed workflows. Useful for implementation, refactoring, and technical problem solving.
Claude CodeCodexOpenClaw
codingdebuggingcode-review

$19.99

One-time license

View Skill
Developer Kit
API Documentation Generator
Generates structured, developer-ready API documentation from code, OpenAPI specs, route definitions, or descriptions. Produces reference docs, quickstart guides, error references, and code examples.
Claude CodeCodexOpenClaw
apidocumentationdeveloper-experience

$19.99

One-time license

View Skill
Developer Kit
Intelligent PR Composer
Generates pull request descriptions that capture context, alternatives considered, test plan, risk areas, and reviewer guidance beyond a simple diff summary. Useful for teams that want senior-quality PRs without manual authoring.
Claude CodeCodexOpenClaw
pull-requestscode-reviewgit

$19.99

One-time license

View Skill