Skip to main content
Back to case studies
accessibilitysaasreactnode.js

A11yPilot: Designing a Full-Stack Accessibility Management Platform

Cover image for A11yPilot: Designing a Full-Stack Accessibility Management Platform
3
Scan Modes
27
Components Documented
40+
Views / Pages
97%
Own A11y Score
0on own scan
Critical Violations
<1.2s
Cold Page Load

Project Overview

A11yPilot is a full-stack accessibility management platform built for design and engineering teams who need more than a one-time audit tool. It combines automated scanning, AI-powered analysis, WCAG reference documentation, a living component library, design token management, and team collaboration — all in a single, cohesive product.

The platform serves two mental models simultaneously: developers who need precise violation data, WCAG criterion links, and fixable code snippets; and designers who need visual contrast checks, component accessibility guidance, and standards-aligned design tokens.

3URL · HTML · Screenshot
Scan Modes
27with live previews
Components Documented
78+criteria referenced
WCAG Criteria
40+REST routes
API Endpoints
40+in the app
Views / Pages
97%0 critical/serious
Own A11y Score

Building an accessibility tool that is itself inaccessible would be the ultimate irony. Every design decision in A11yPilot had to meet the same WCAG 2.2 AA bar we ask of the products our users bring to it.

Shahriar Shanto

The Problem

The Fragmented A11y Workflow

Accessibility work in most teams follows the same painful loop:

  1. Run a browser extension audit (axe, WAVE, Lighthouse)
  2. Export a spreadsheet of violations
  3. Paste WCAG criteria links into Jira tickets
  4. Manually cross-reference component docs
  5. Re-scan after fixes — with a different tool, different results
  6. Repeat endlessly without institutional memory

There is no single source of truth. Violation data lives in spreadsheets. WCAG references live in browser tabs. Component guidance lives in a Notion page nobody reads. Scan results from last month are gone.

🔴 The result

Accessibility debt accumulates faster than it can be paid down, and teams have no visibility into whether they are getting better or worse over time.

Who Suffers

  • Front-end engineers spend hours correlating axe violation IDs with WCAG criteria and finding the right component docs
  • QA engineers re-run the same scans on every PR without a structured comparison workflow
  • Designers have no accessible design token system and rely on manual contrast checking
  • Product managers have no score or trend to track accessibility health sprint-over-sprint
  • Accessibility leads cannot enforce consistent standards across teams because there is no shared ruleset

Goals & Success Criteria

GoalMeasure of Success
Unify scanning, reference, and component guidanceAll three accessible from a single authenticated session
Provide trend visibilityScore card showing per-scan scores over time with delta
Make WCAG actionable at the component levelEvery violation links directly to its component's accessibility guide
Support both URL and code-paste scanningTwo scan modes without switching tools
Enforce standards at the system levelAdmin-managed rules library synced from W3C/Section 508
Be accessible by WCAG 2.2 AA itselfPass its own scanner with 0 critical/serious violations
Support dark and light working environmentsFull dark mode with all text meeting 4.5:1 contrast

Research & Discovery

User Interviews — Three Primary Personas

Research drew from conversations with front-end engineers, QA engineers, senior UX designers, and accessibility specialists across SaaS product teams.

👩‍💻 Persona 1 — The Developer (Dev Dani)

Senior front-end engineer, 4 years exp. Runs axe DevTools daily but loses context between scans. Frustrated by WCAG link dumps without actionable component guidance. Wants to see which violations are regression vs. new. Key need: "Tell me exactly which component broke and how to fix it"

🔍 Persona 2 — The QA Specialist (QA Quinn)

Accessibility QA engineer. Owns the accessibility audit spreadsheet (1,200+ rows). Re-runs scans manually before every release. Needs scan comparison between builds. Key need: "I need to prove we are getting better, not just that we passed today"

🎨 Persona 3 — The Designer (Design Dana)

Senior UX/UI designer. Checks contrast manually with online tools. Does not know which design tokens map to which WCAG criterion. Key need: "I want to build accessible components from scratch, not audit them after handoff"


Competitive Analysis

ToolScanningComponent DocsDesign TokensTrend TrackingTeam Collab
axe DevTools
WAVE
Deque WorldSpacePartialBasic
Stark (Figma)Partial
A11yPilot

💡 The gap that became the product vision

No existing tool combined scanning + documentation + design tokens + history in one product. That gap defined A11yPilot's scope.

Information Architecture

Core Navigation Structure — 40+ Views

A11yPilot
│
├── Dashboard                     ← Health overview, recent scans, quick access
│
├── Standards & Guidelines
│   ├── WCAG 2.1/2.2             ← Full criterion reference
│   ├── Section 508              ← Federal compliance reference
│   ├── International Standards  ← EN 301 549, global standards
│   ├── Compliance Checklists    ← Pre-built QA checklists
│   └── Rules Management         ← Import rules from W3C/Section 508 URLs
│
├── Component Library             ← 27 accessible components with live previews
│   └── [Component Detail]        ← Code, variants, WCAG mapping, keyboard nav
│
├── Testing & Validation
│   ├── Accessibility Scanner     ← URL / HTML / Screenshot scan
│   ├── Scan History              ← All past scans with full detail modal
│   ├── Scan Compare              ← Diff two scans side-by-side
│   ├── Violation Patterns        ← Cross-scan violation trend analysis
│   └── SR Simulation             ← Visual screen reader output simulation
│
├── Resources
│   ├── Learning Path             ← Structured a11y learning curriculum
│   ├── CI/CD & Integrations      ← GitHub Actions, CI pipeline guides
│   ├── Discussions               ← Threaded team collaboration
│   └── Design Tokens             ← Token library with a11y mappings
│
├── Design System
│   ├── DS Generator              ← AI-powered design system scaffolding
│   ├── DS History                ← Snapshot history
│   └── DS Report                 ← Export design system documentation
│
├── Interactive Tools
│   ├── Contrast Checker          ← Real-time WCAG contrast ratio tool
│   ├── Keyboard Simulator        ← Keyboard interaction tester
│   └── Code Editor               ← Live accessible code sandbox
│
└── Admin Module (admin-only)
    ├── Dashboard                  ← Platform usage analytics
    ├── User Management
    ├── Standards Management
    └── System Settings

Navigation Design Decisions

Collapsible sidebar groups were chosen over a flat nav because the feature count (40+ views) exceeds what a top nav can hold. Power users (QA engineers, a11y leads) navigate between sections constantly — they need persistent, glanceable nav. Collapsing rarely-used sections keeps the primary workflow clean.

Breadcrumb trail added above every page to prevent disorientation in a deep navigation tree.

Active state persistence — each sidebar group remembers its open/closed state per session so returning users don't lose their workflow context.

UX Design

Design Principles

  1. Accessible by default, not by audit — every interactive component designed to WCAG 2.2 AA standards. Focus visible states use a 3px offset ring in the brand green. All text meets 4.5:1 on both light and dark surfaces. Touch targets ≥ 44×44px.
  2. Density without clutter — the design uses information density typical of developer tools while maintaining visual breathing room through consistent 12px/16px vertical rhythm, section dividers, and card-based layout.
  3. Dark-first — default theme is dark (#111827 sidebar, #1e293b content surface, #0f172a page background). This matches the environment of the primary user — engineers working in dark IDEs. Light mode is a full first-class implementation.
  4. Progressive disclosure — complex data (violation details, AI findings, component code) is hidden behind expand or modal interactions. The primary table view shows the minimum needed to decide whether to drill in.

Dashboard — Answering Three Questions in Under 10 Seconds

  1. How healthy is my product right now? → Accessibility Health card with score circle + trend bars
  2. What broke most often? → Top System Violations table
  3. What did I scan recently? → Recent Scans list

📐 Dashboard Grid — Design Evolution

First iteration: three equal-width columns. Problem: the score got the same visual weight as secondary data. Final iteration: asymmetric grid — auto 1fr. The score circle takes natural width (~82px), trend bars fill remaining space. This gives the score visual primacy while keeping all four severity counts visible without scrolling.


Component Library — The Core Innovation

The Component Library is what differentiates A11yPilot most from audit-only tools. Each component page contains:

  1. Live preview — interactive component rendered in the browser, not a screenshot
  2. Variant selector — toggle between variants using accessible aria-pressed toggle buttons (not dropdowns)
  3. Code tab — copy-ready HTML/JSX with ARIA attributes pre-applied
  4. WCAG mapping — which success criteria apply and how the component addresses them
  5. Keyboard navigation guide — exact key behavior documented
  6. Screen reader output — expected announcements for NVDA, JAWS, VoiceOver
  7. Do / Don't examples — visual contrast between correct and incorrect implementations

🔗 Scan-to-component shortcut

When a scanner violation is detected (e.g. button-name, image-alt, color-contrast), the violation detail links directly to the relevant component page. This closes the loop between 'what broke' and 'how to fix it' — typically 2 clicks.


Accessibility Scanner — Three Modes, One Interface

Mode 1 — URL Scan: Enter any public URL. Puppeteer renders the page in a headless Chrome instance, injects axe-core, and runs the full ruleset. Returns violations, passes, and incomplete items with node-level HTML snippets.

Mode 2 — HTML Paste: Paste raw HTML markup. jsdom parses it and runs axe-core against the in-memory DOM. Useful for testing individual component markup, email templates, or code that isn't publicly deployed.

Mode 3 — Screenshot: Upload a screenshot. Google Gemini Vision analyzes the image for visual accessibility issues — contrast problems, missing focus indicators, text size concerns, color-only information.

🎯 UX decision — single interface, three modes

Rather than three separate pages, all three modes live in one scanner interface with a radio/tab selector at the top. This mirrors how a real QA session works — you might run URL scan, then paste a component fragment, in the same sitting.


Rules Management — Defining Your Standard

Teams use different subsets of WCAG. Some are bound by Section 508 (federal). Some apply WCAG 2.2 AAA for healthcare. Rules Management allows teams to:

  1. Click a preset (WCAG 2.1, WCAG 2.2, Section 508) — the system fetches the W3C or Access Board source URL, parses it with jsdom, and extracts all success criteria
  2. Enter any URL — fetch rules from any accessibility standards document
  3. Review before importing — a modal shows all found rules with checkboxes; import all or select specific criteria
  4. Add manually — create custom rules for internal standards not covered by public docs

🔢 Review modal UX

Showing 78 WCAG success criteria at once risks overwhelm. The modal uses a Select All toggle + individual checkboxes, letting the user deselect what doesn't apply. The count updates live: '34 of 78 selected.'

Design System & Architecture

Color System — Emerald as Primary

The primary color is Emerald (#10b981 / #059669). Chosen for:

  • Clear semantic distance from red (error) and amber (warning) — no confusion in an accessibility context
  • Strong performance in both light and dark surfaces
  • WCAG AA compliance: #059669 on white (4.62:1) and #10b981 on #111827 dark (5.23:1)

Three-layer token structure:

  • Layer 1 — Primitives: Raw values (--prim-gray-900: #111827)
  • Layer 2 — Semantic tokens: Role-based (--text-strong, --fill-brand-strong, --stroke-neutral)
  • Layer 3 — Component tokens: Scoped (--btn-primary-bg, --input-focus-ring, --sidebar-active-bg)
RoleFontWeightSize
BodyInter40014–16px
UI labelsInter500–60011–14px
Section headersInter70010px uppercase + tracked
Page titlesInter70020–24px
CodeJetBrains Mono40012–13px

System Architecture

┌─────────────────────────────────────────────────────────┐
│                      CLIENT (React)                      │
│   Vite · React 18 · TypeScript · Tailwind · Flowbite    │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐  │
│  │Dashboard │  │ Scanner  │  │Components│  │ Admin  │  │
│  └──────────┘  └──────────┘  └──────────┘  └────────┘  │
│                    Axios + JWT interceptor                │
└─────────────────────────┬───────────────────────────────┘
                          │ HTTPS
┌─────────────────────────▼───────────────────────────────┐
│                  SERVER (Node.js / Express)               │
│                                                          │
│  /auth  /scanner  /standards  /rules  /components       │
│  /tokens  /users                                        │
│                                                          │
│  Middleware: auth · adminOnly · rate-limit · helmet      │
└──────┬────────────────────┬────────────────────┬────────┘
       │                    │                    │
┌──────▼──────┐    ┌────────▼──────┐   ┌────────▼──────┐
│   MongoDB   │    │  axe-core +   │   │  Groq / Gemini │
│  (Mongoose) │    │  Puppeteer +  │   │   AI APIs      │
│             │    │  jsdom        │   │                │
└─────────────┘    └───────────────┘   └────────────────┘

Security Implementation

  • Authentication: JWT with jsonwebtoken. Access tokens injected via Axios interceptor. Admin-only routes protected by adminOnly middleware.
  • Rate limiting (per-route): Auth endpoints: 20 req/15 min · Scanner: 15 req/min (Puppeteer cost protection) · Admin: 60 req/15 min · General: 300 req/15 min
  • Security headers: helmet with strict CSP. scriptSrc restricted to 'self' 'unsafe-inline'. HSTS with 1-year max-age.
  • CORS: Allowlist-based. No wildcard.
  • Input limits: Body parser capped at 100KB globally; 2MB only for HTML scan endpoint.

Accessibility of the Accessibility Tool

The unique pressure

There is a particular accountability when building an accessibility platform: it must pass its own audit. A11yPilot scans of A11yPilot itself return 0 critical violations and 0 serious violations.

Measures Taken

  • Focus management: Every modal traps focus on open and returns it to the trigger on close. aria-modal="true" and role="dialog" on all overlays.
  • Skip link: <a href="#main-content">Skip to main content</a> at the top of every page.
  • Semantic HTML: All data tables use <th scope="col">, all forms use explicit <label> associations, all icon-only buttons have aria-label.
  • Color independence: No information conveyed by color alone. Impact badges (critical/serious/moderate/minor) use text labels plus color.
  • Keyboard navigation: Full keyboard operability throughout. Custom components use correct aria-expanded, aria-pressed, and keyboard event handlers.
  • Reduced motion: Sidebar accordion chevron uses CSS transition suppressed by prefers-reduced-motion: reduce.
  • Screen reader tested: Key flows tested with VoiceOver (macOS) and NVDA (Windows).
0on own scan
Critical violations
0on own scan
Serious violations
97%1 moderate only
Own scan score
100%all surfaces
Dark mode coverage
≥44px× 44px
Touch target size
4.5:1WCAG AA
Min contrast ratio

Key Engineering Challenges

Challenge 1 — Tailwind Purge Killing Flowbite Classes

Flowbite React Card component's inner wrapper uses p-6 in its internal theme — but this class was not in any source .tsx files. Tailwind's content scan pointed to node_modules/flowbite-react/lib/**, a directory that doesn't exist (Flowbite ships in dist/). Result: p-6 was purged from the production CSS bundle, making every Card appear with 0px padding.

🔧 Fix

1. Updated content path from lib/** to dist/**/*.{js,cjs,mjs}. 2. Added explicit safelist for Flowbite-internal classes: ['p-6', 'p-4', 'p-5', 'gap-4', 'gap-6', 'justify-center', 'flex-col', 'h-full', 'rounded-t-lg']

📚 Lesson

When a UI library generates classes internally (not via your TSX source), those classes are invisible to Tailwind's content scanner. Always trace the actual build output path of any UI library in your content config.


Challenge 2 — CSS Specificity: .dark a vs text-white

The global dark mode stylesheet had .dark a { color: #60a5fa } for link legibility. But call-to-action <Link> components using text-white were overridden — rendering white CTAs as blue.

Root cause: Tailwind utilities have specificity [0,1,0] (one class). The .dark a rule has specificity [0,1,1] (class + element) — one point higher. Tailwind lost every time.

🔧 Fix

Applied inline style={{ color: '#ffffff' }} on specific CTA Links. Inline styles have specificity [1,0,0] — highest possible without !important.


Challenge 3 — Modal Hidden Behind Sticky Header

The scan detail modal used fixed inset-0 (top: 0) with my-8 (32px card margin). The sticky header is 60px tall. Net result: the top 28px of the modal card — including the scan URL title and close button — was hidden behind the header.

🔧 Fix

Changed overlay from fixed inset-0 to fixed inset-x-0 bottom-0 with style={{ top: 'var(--header-height)' }}. The overlay now starts exactly at the header's bottom edge. Added click-outside-to-dismiss by checking e.target === e.currentTarget on the overlay onClick.


Challenge 4 — Flowbite Table Generating Invisible Rows in Dark Mode

Flowbite's <Table> component adds className="bg-white" to every <TableRow> via its internal theme system — regardless of what className prop you pass. In dark mode, white rows on a dark surface made all table content invisible.

Attempts that failed:

  • Adding dark:bg-gray-800 to <TableRow> — ignored, Flowbite theme overrides it
  • CSS override .dark tr { background: #1f2937 } — specificity too low vs Flowbite's inline class

🔧 Final fix

Replace Flowbite Table entirely with plain table/tr/td HTML using explicit Tailwind dark variant classes. Zero Flowbite involvement = zero surprise class injection. Pattern adopted project-wide: all data tables now use plain HTML table elements.

Results & Outcomes

3URL · HTML · Screenshot
Scan modules
27with live previews
Components documented
78+872.1 + 2.2
WCAG criteria
97%0 critical/serious
Own accessibility score
~48KBproduction
CSS bundle (gzipped)
< 1.2sVite production build
Cold page load

Design Achievements

  • Unified 40+ views under a single coherent design system with no Bootstrap dependency
  • Achieved full dark/light mode with documented color tokens at each layer
  • Component library covers 27 components with live previews — none require a design tool to view
  • The scan-to-component-guide link is a zero-friction path from "violation detected" to "how to fix" — typically 2 clicks

Technical Achievements

  • Single codebase handles URL scanning (Puppeteer), HTML scanning (jsdom), and AI image analysis (Gemini) — no external scraping service dependency
  • Rules fetching engine works against W3C WCAG pages, Section 508, and generic HTML documents
  • JWT + role-based admin system with route-level protection on both client and server
  • MongoDB schema supports multi-standard rules (WCAG A/AA/AAA and Section 508) in a single collection

Lessons Learned

🏗️ 1. Build the design system first

The CSS token system (three-layer primitives → semantic → component) paid enormous dividends. When a dark mode bug appeared, there was a clear place to fix it: the semantic token. No hunting through component files.

⚠️ 2. Distrust UI library internals

Flowbite React generates classes in ways Tailwind's purge cannot see and your JSX cannot override. Understanding how a library generates its output before committing to it would have saved several debugging sessions.

📌 3. The sticky header z-index trap

Any position: fixed overlay using inset-0 will have its top edge behind your sticky header. Always account for header height in modal/drawer positioning.

4. Accessibility is design debt, not QA debt

Every accessibility fix made after a component was built took 5–10× longer than if the accessibility pattern had been part of the original component template. The component library now encodes correct patterns, preventing the debt from accumulating.

🔬 5. Meta-accessibility is real accountability

Knowing A11yPilot would scan itself created genuine accountability for every design decision. If you would not submit it to your own tool, do not ship it.


What's Next

  • Real-time violation alerts: Webhook integration with CI/CD to post scan results to Slack/Teams on PR merge
  • ARIA pattern library: Extend component docs with complex ARIA patterns (combobox with listbox, tree view, live regions)
  • Bulk URL scanning: Queue-based scanner for scanning entire sitemaps
  • Export to VPAT: Generate Voluntary Product Accessibility Template from scan history
  • SSO integration: SAML/OAuth for enterprise team authentication
  • Figma plugin: Surface component accessibility guidance directly in the design tool