Features β’ Verified-Fix Engine β’ Chrome Extension β’ Tech Stack β’ How To Run β’ License
Not just a list of problems β the exact code to paste, proven to work. Each fix is applied in the page and the audit is re-run to confirm it clears the violation before it's suggested.
π Live demo: access-check.marianacastro.dev Β· π§© Chrome extension: Chrome Web Store
A tool that measures contrast shouldn't have questionable contrast of its own, so the landing page is audited with the same engine on every change.
- The product interface passes: 0 automated contrast failures. Every eyebrow, label, form field, section heading and button in the real UI clears WCAG AA (4.5:1, or 3:1 for large text), checked with axe-core.
- The single contrast finding the audit reports lives inside the demo mock-up: the illustrated
aurora-coffee.comsample page, whose low-contrast "Order now" button is exactly the barrier AccessCheck is built to detect. - Fixes that came straight out of this self-audit: darker
mutedandserioustext tokens (AA on every background), a readable input placeholder, a solid hero background so text is never measured over a gradient, and light text on the dark call-to-action.
| π Whole-Site Crawl | Point it at a domain and it discovers pages from the sitemap (or by crawling links) and audits them in parallel β a background job fans out one short serverless run per page, with live progress and an aggregate site score. |
| π§ Copy-Paste Fixes | Each violation gets a generated code snippet β the exact contrast color, alt text, or label to paste β not just a restated rule. |
| β Verified Fixes | Every fix is applied in the page and the audit is re-run to prove it actually clears the violation before it's suggested. |
| βοΈ Verdict & Export | Where the page stands β blocked, failing, minor gaps or no rule failed β a prioritized "Fix First" list, and an exportable PDF / Markdown report you can hand to a client or paste into a ticket. |
| π Beyond Violations | Surfaces axe's "best practice" recommendations and flags items that need manual review β the two buckets most tools silently discard. |
| β¨οΈ Keyboard Path | Tabs through the page in a real browser and maps the focus order β flagging invisible focus, keyboard traps, positive tabindex, and controls that can't be reached by keyboard. |
| π± Context-Aware Scan | Re-audits at a mobile viewport and after opening menus / disclosures, catching violations that only surface on small screens or once the UI is expanded. |
| π Change Over Time | Signed-in audits are saved, and each new one is diffed rule by rule against the last β which barriers cleared, which came back, and whether that moved where the page stands. A page that passes today can fail after the next deploy. |
| π§© Chrome Extension | A Manifest V3 side panel that audits the tab you are already on β including a keyboard focus path walked with real Tab presses β and points at the offending element in the live page. |
| Desktop | Mobile |
![]() |
![]() |
![]() |
Change tracking, as explained on the landing page β an example page moving from failing to minor gaps, with two rules cleared and one regression.
The side panel: where the current tab stands, and the focus path drawn over the live page.
| Category | Technologies |
|---|---|
| Framework | Next.js 16 (App Router), React 19 |
| Language | TypeScript 5 |
| Styling | Tailwind CSS v4 |
| Audit Engine | axe-core, Playwright (playwright-core + @sparticuz/chromium) |
| Database | PostgreSQL (Neon, serverless driver) + Prisma 7 |
| Authentication | Auth.js / NextAuth v5 (GitHub, Google β OAuth only) |
| Cache & Rate Limit | Upstash Redis (HTTP-based, shared across instances) |
| Background Jobs | Upstash QStash (fan-out of the multi-page crawl, one run/page) |
| Browser Extension | Chrome Manifest V3 (side panel, chrome.debugger / CDP), esbuild |
| Testing | Vitest, Playwright-driven gates |
| Tooling | ESLint, Prettier |
AccessCheck is an accessibility auditor that goes one step further than the usual checker. Most tools tell you what is broken; AccessCheck generates the exact code to fix each violation, proves the fix works by re-running the audit after applying it, and groups repeated issues so one change can resolve many elements at once.
It renders the page in a real headless browser (Playwright), runs axe-core against WCAG 2.2 A/AA rules, and turns the raw findings into an actionable report β a screenshot with issue markers, the keyboard focus path as a readable list, context re-scans (mobile + expanded UI), a plain-language verdict, a prioritized "Fix First" list, and an exportable PDF.
The scan runs server-side in a Node runtime (/api/scan) because Playwright needs a real browser. Locally it uses the full Playwright Chromium; on serverless it falls back to playwright-core + @sparticuz/chromium. axe-core is injected into the target page with bypassCSP enabled, so the audit still runs on sites that ship a strict Content-Security-Policy (which would otherwise block third-party script injection).
Additional features:
- Verified, copy-paste remediation: The flagship feature. See the dedicated section below β each fix is deterministically generated, applied to the live DOM, and re-audited to label it Verified or Needs review before it's ever suggested.
- Whole-site crawl (background job): Point it at a domain and AccessCheck discovers pages from the sitemap (falling back to a same-origin link crawl), then audits them in parallel. Because a single scan already spends 10β25s in a headless browser and Vercel caps a function at 60s, the crawl is decomposed into a durable background job: Upstash QStash fans out one short serverless invocation per page, each writes its result to Postgres, and the client polls a live progress view that ends in an aggregate site reading. It degrades gracefully β with no QStash configured (local dev) the pages are processed inline instead. Works signed-in or anonymous.
- Scan history with diffs: Signed-in scans are saved at
/history(newest first, with thumbnails and a delta vs the previous scan of the same URL). Opening a saved report shows a "Changes since last scan" panel β exactly which rules were fixed or regressed over time, computed by a pure, unit-tested diff function. - Three-layer result reuse & rate limiting: A page audited in the last 5 minutes isn't audited again. The browser session answers first (instant, survives leaving the report and coming back, or hopping to the PDF export), then Upstash Redis for anonymous readers (shared across serverless instances, screenshot included so a hit still opens with its evidence frame), then the reader's own scan history in Postgres when signed in. A reused reading never poses as a new one β it carries the
scannedAtof the run that produced it and the report says how old it is, with Re-audit walking past every layer. Scans are gated at 5/min per IP via the same Redis, and everything degrades gracefully to measuring again when Redis isn't configured. - Keyboard focus-path analysis: Every scan tabs through the page in the real browser, maps the focus order, and flags invisible focus indicators, keyboard traps, positive
tabindex, and interactive controls that can't be reached by keyboard β operability checks axe-core doesn't perform. - Context-aware re-scans: Beyond the default desktop pass, AccessCheck re-audits the page at a mobile viewport and after opening menus / disclosures, surfacing violations that only appear on small screens or once dynamic UI is expanded.
- Three-tier reporting: Results are split into WCAG violations (confirmed failures), best practices (recommendations beyond the spec, clearly labelled as non-blocking), and needs-manual-review items (axe flagged something but can't decide automatically β surfaced with the affected selectors plus a per-rule, step-by-step walkthrough of how to confirm it by hand) so you know exactly where to look. Most tools collapse these into one list or discard tiers 2 and 3 entirely.
- Export to PDF & Markdown: A formatted report view at
/report, plus a browser-generated Markdown report (verdict, severity table, Fix First list, every violation with its fix and verification status) perfect for pasting into an issue, PR, or ticket. - Responsive layout: Fully responsive across the landing, results, and exportable report views.
The remediation isn't a list of generic advice β it's a small engine that is concrete (it writes the code) and verified (it proves the code works before suggesting it). The scan pipeline runs server-side:
URL β headless Chromium (Playwright) β inject axe-core β WCAG audit
β deterministic fix generation (per node)
β cluster identical fixes into groups
β re-run axe per fix to verify it clears the violation
β keyboard focus-path pass + mobile / dynamic-state re-scans
β verdict + markers + report β UI / PDF
It writes the actual fix. Instead of restating the rule, each violation gets a generated snippet from a deterministic, dependency-free generator (src/lib/scan/remediate.ts):
- Color contrast β computes the nearest passing text color to the original via binary search (on rounded RGB, so the suggested hex actually passes the target ratio) rather than dumping pure black/white.
- Missing alt text β infers a description with a cascade: element
titleβ surrounding context (figcaption / wrapping link) β filename (stripping@2xand extensions), falling back toalt=""for decorative images. - Form labels & accessible names β suggests a
<label for>(when there's an id) or anaria-label, guessing the text from placeholder / name / id; covers buttons, links, and ARIA controls without a name. - Document-level & ARIA β missing
<html lang>, missing<title>, zoom-blocking viewport, and the exact required-but-missing or not-allowed ARIA attributes axe reports.
It proves the fix, doesn't assert it. A deterministic fix β one computed from measured values, in practice a contrast colour β carries a structured DOM mutation. AccessCheck applies that mutation in the page, re-runs axe scoped to that one rule, then reverts, and reports only what the re-run showed:
- Verified fix β the rule stopped flagging the element.
- Needs review β the change was applied and the rule still flags it. The suggestion isn't enough on its own.
Everything else β an alt text, an aria-label, a structural change β is a suggestion that depends on what the page means, and no re-run can settle it. Those findings carry no verification label at all: they read as what they are, a suggestion that needs a person. Measured over eight production pages: 55 findings, 4 verified, 2 needing review, 49 in that third group β which is why it is the quiet one rather than a badge repeated on every card. The distinction is still in the data (verified / failed / unchecked per fix group); it is the interface that stopped spending attention on it.
It groups what repeats. When many nodes of the same violation share an identical fix (e.g. 14 buttons with the same contrast problem), they collapse into a single group β "Resolves N elements" β validated once per group via a representative selector. This turns "here are 14 problems" into "here is 1 fix that clears 14 elements".
The web app audits a URL. The extension audits the tab you are already looking at β behind a login, mid-checkout, three clicks into a flow no crawler can reach. It is a Manifest V3 side panel that reuses the same engine as the server-side scan.
Install it from the Chrome Web Store.
- The debugger is opt-in, not the price of admission. Clicking the icon runs the rules, the own-rule audits and the fix verification without ever attaching
chrome.debugger. Walking the keyboard focus path is a separate action that states its cost before you press it, and Continue the walk resumes a walk that was cut short instead of starting over. - A real focus path, not a simulated one. The walk attaches
chrome.debuggerand dispatches genuineTab/Shift+Tabthrough CDP, so the order it reports is the order Chrome actually produces β including what the browser skips. The debugger is attached for that step only and released before the report appears; a walk that is cut short still releases it. - It names the element, not just its CSS path. axe points at
.bg-gradient-left.dark\:bg-gradient-left-darkβ¦β 501 characters of compiled Tailwind on react.dev. The report readsspan.text-gray-30 βexample.com/β Β· in <article>, built from the tag, one stable attribute, the accessible name and the enclosing landmark, with generated ids and hashed classes deliberately left out. Repeated elements that would read the same way are numbered (1 of 3) rather than invented. The selector is kept beside the name for Locate on page and Copy selector β identity is for people, the locator is forquerySelector. - It says what it could not check. Coverage is stated next to the verdict ("Partial coverage Β· 3 checks unavailable") rather than quietly rounded away, and the reading is never called complete when checks were skipped.
- Narrow permissions.
activeTab,scripting,sidePanel,storage,debuggerβ no host permissions, and nothing is loaded over the network at runtime.
The panel is held to the standard the product sells: one <main>, one <h1>, no skipped heading levels, a visible focus ring on every control, no horizontal overflow at 320β600px or at 200% zoom, and no layout shift when a finding opens β all asserted by scripts/check-panel-ux.mjs.
The most challenging part of this project was making the remediation trustworthy rather than just plausible. Generating a fix is easy; proving it actually clears the violation meant building a structured apply-and-revert layer over a live DOM and re-running the audit scoped to a single rule. Getting the contrast math right β guaranteeing the suggested color passes its WCAG target after rounding β pushed me toward property-based testing. The deterministic core (color math, fix generators, scoring, grouping, and the history diff) is fully unit-tested with Vitest, ensuring reliability and maintainability of the codebase.
The second one was making the extension and the server agree. Two audits of the same page that disagree are worse than one audit, so there is only ever one engine: src/lib/scan/dom/engine.ts is bundled by esbuild into a single IIFE (dom-engine/dom-engine.js) with its sha256 written beside it, and both drivers load that identical artifact β Playwright injects it server-side, the extension ships it as a file. A parity gate then audits the same fixtures through both and fails the build if the focus paths, selectors, markup or measured rectangles diverge, so the two can't quietly drift apart. The packaged release is checked the same way: the archive is rejected if any file in it differs from a fresh build.
Clone the repository:
git clone https://github.com/maricastroc/access-checkInstall the dependencies:
npm installRename the .env.example file to .env and add the necessary information to it. (The app runs without a database, Redis, or OAuth configured β those only enable history, caching, and sign-in.)
Start the service:
npm run devRun all tests:
npm run testβ© Access http://localhost:3000 to view the web application.
To just use it, install the published version from the Chrome Web Store. To run it from source, build it:
npm run build:extensionThen open
chrome://extensions, turn on Developer mode, choose Load unpacked and select theextension/distfolder. Click the AccessCheck icon on any tab to open the side panel.
Run the browser gates (each drives a real Chrome through Playwright):
npm run check:manifest && npm run check:extension && npm run check:deep && npm run check:panelAnd the one for the web report, which builds the app and drives it against a fixed result covering every capture state:
npm run check:resultsReleased under the MIT License. You're free to use, study, fork and build on this code β as long as the original copyright and license notice are kept. Reuse it and learn from it; don't strip the attribution and present it as your own.
Β© 2025β2026 Mariana Castro Β· Live demo Β· Chrome extension
β If you like this project, give it a star on GitHub!






