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.
One-Time Purchase
$19.99
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 overrole="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
$19.99
One-time license
$19.99
One-time license
$19.99
One-time license