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:
- Run a browser extension audit (axe, WAVE, Lighthouse)
- Export a spreadsheet of violations
- Paste WCAG criteria links into Jira tickets
- Manually cross-reference component docs
- Re-scan after fixes — with a different tool, different results
- 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
| Goal | Measure of Success |
|---|---|
| Unify scanning, reference, and component guidance | All three accessible from a single authenticated session |
| Provide trend visibility | Score card showing per-scan scores over time with delta |
| Make WCAG actionable at the component level | Every violation links directly to its component's accessibility guide |
| Support both URL and code-paste scanning | Two scan modes without switching tools |
| Enforce standards at the system level | Admin-managed rules library synced from W3C/Section 508 |
| Be accessible by WCAG 2.2 AA itself | Pass its own scanner with 0 critical/serious violations |
| Support dark and light working environments | Full 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
| Tool | Scanning | Component Docs | Design Tokens | Trend Tracking | Team Collab |
|---|---|---|---|---|---|
| axe DevTools | ✅ | ❌ | ❌ | ❌ | ❌ |
| WAVE | ✅ | ❌ | ❌ | ❌ | ❌ |
| Deque WorldSpace | ✅ | Partial | ❌ | Basic | ✅ |
| 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 SettingsNavigation 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
- 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.
- 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.
- Dark-first — default theme is dark (
#111827sidebar,#1e293bcontent surface,#0f172apage background). This matches the environment of the primary user — engineers working in dark IDEs. Light mode is a full first-class implementation. - 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
- How healthy is my product right now? → Accessibility Health card with score circle + trend bars
- What broke most often? → Top System Violations table
- 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:
- Live preview — interactive component rendered in the browser, not a screenshot
- Variant selector — toggle between variants using accessible
aria-pressedtoggle buttons (not dropdowns) - Code tab — copy-ready HTML/JSX with ARIA attributes pre-applied
- WCAG mapping — which success criteria apply and how the component addresses them
- Keyboard navigation guide — exact key behavior documented
- Screen reader output — expected announcements for NVDA, JAWS, VoiceOver
- 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:
- 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
- Enter any URL — fetch rules from any accessibility standards document
- Review before importing — a modal shows all found rules with checkboxes; import all or select specific criteria
- 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:
#059669on white (4.62:1) and#10b981on#111827dark (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)
| Role | Font | Weight | Size |
|---|---|---|---|
| Body | Inter | 400 | 14–16px |
| UI labels | Inter | 500–600 | 11–14px |
| Section headers | Inter | 700 | 10px uppercase + tracked |
| Page titles | Inter | 700 | 20–24px |
| Code | JetBrains Mono | 400 | 12–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 byadminOnlymiddleware. - 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:
helmetwith strict CSP.scriptSrcrestricted 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"androle="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 havearia-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
transitionsuppressed byprefers-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-800to<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