Accessibility checks beyond syntax
Rules confirm that a page has a title, a link has text and an image has alt. They can’t tell that “Untitled document”, “Click here” or alt="img-1" say nothing. Rampa runs axe-core, asks a model only about what rules can’t decide, and keeps a finding only when its evidence checks out on the page.
Open source under the MIT license. Works with a local model or any major provider.
Rules see the attribute, not what it says
In February 2026, 95.9% of the top million home pages had WCAG failures that automated tools detect, according to the WebAIM Million. Those are the easy ones.
A title that says “Untitled document”, a lone “Click here”, a heading “Section 2” over customer reviews, an alt that names the wrong thing: rule-based checkers pass them all, because the element is there.
<img src="dog.svg" alt="img-1">alt="img-1".- axe-core
Passes
The image has alternative text. rule image-alt
- Rampa
Fails
The text alternative "img-1" is a file name or a placeholder.
Evidence, found on the page:
img-1Suggested in the page’s language:
alt="Ilustração de um cachorro marrom com coleira vermelha"
A suggestion is for a person to review. Rampa never rewrites your page.
Five more that every rule passes
| On the page | axe-core | Rampa |
|---|---|---|
WCAG 2.4.2 <title>Untitled document</title> | passes | The page title “Untitled document” says nothing about this page. |
WCAG 2.4.4 <a href="shipping.html">Click here</a> | passes | The link text “Click here” does not tell where the link goes, and nothing around it does. |
WCAG 2.4.6 <h2>Section 2</h2> | passes | The heading “Section 2” says nothing about the content under it. |
WCAG 2.4.6 <label for="email">Field 1</label> | passes | The label “Field 1” does not say what to enter. |
WCAG 3.1.1 <html lang="en">on a page in Portuguese | passes | The page is marked lang="en", but most of its text is in Portuguese (pt). |
Each line is a real finding from the store and market examples in the repository, each with its evidence and a patch.
One criterion at a time, and only with evidence
Every run goes through five stages. The model only sees what the stages before it could not settle.
Collect
The page becomes a normalized accessibility snapshot: roles, names, languages, states, and every image as it was rendered.
Rules first
axe-core runs on the page. Whatever its rules can decide goes straight to the report, with no model involved.
Judge the residue
What rules can’t decide goes to a model: one prompt per WCAG criterion, the smallest context that answers it, and a typed answer back.
Verify the evidence
Every claim must cite a node that exists and text that is really in it. A claim that fails is dropped and counted, never shown.
Report the coverage
Findings come with a patch, and the report lists what nobody checked. It never calls a page accessible.
Run it on a page
A store page with nine problems. axe-core catches the logo without alt text; the other eight pass every rule: three alt texts, the page title, a link, a heading, a field label and a quote in the wrong language. This is the real output of a run on a local model.
rampa check examples/store/before.html examples/store/before.html Surface: web · axe-core 4.14.0 · Model: ollama:gemma4:12b WCAG 1.1.1 (A) — Non-text Content ✗ html > body > header > img Images must have alternative text high · rule image-alt ✗ html > body > main > div > figure:nth-of-type(1) > img The text alternative "IMG_2034.jpg" is a file name or a placeholder. Evidence: "IMG_2034.jpg" Patch: - <img src="mug.svg" alt="IMG_2034.jpg"> + <img src="mug.svg" alt="Blue mug with steam"> confidence high · 1/1 runs · evidence verified · id 2c1d064d9cc7 ✗ html > body > main > div > figure:nth-of-type(2) > img The text alternative "Ceramic coffee mug" describes something the image does not show. Evidence: "Ceramic coffee mug" Patch: - <img src="umbrella.svg" alt="Ceramic coffee mug"> + <img src="umbrella.svg" alt="Pink umbrella in the rain"> confidence high · 1/1 runs · evidence verified · id 55249d898b55 ✗ html > body > main > div > figure:nth-of-type(3) > img The text alternative "product" is too generic to describe the image. Evidence: "product" Patch: - <img src="plant.svg" alt="product"> + <img src="plant.svg" alt="Potted plant"> confidence high · 1/1 runs · evidence verified · id a2e0ae93ad7c WCAG 2.4.2 (A) — Page Titled ✗ html The page title "Untitled document" says nothing about this page. Evidence: "Untitled document" Patch: - <title>Untitled document</title> + <title>Corner Store - New Arrivals</title> confidence high · 1/1 runs · evidence verified · id a7fe7f746d48 WCAG 2.4.4 (A) — Link Purpose (In Context) ✗ html > body > main > p:nth-of-type(2) > a The link text "Click here" does not tell where the link goes, and nothing around it does. Evidence: "Click here" Patch: - <a href="shipping.html">Click here</a> + <a href="shipping.html">View shipping information</a> confidence high · 1/1 runs · evidence verified · id 1af8a73e209d WCAG 2.4.6 (AA) — Headings and Labels ✗ html > body > main > h2:nth-of-type(1) The heading "Section 2" says nothing about the content under it. Evidence: "Section 2" Patch: - <h2>Section 2</h2> + <h2>Customer Reviews</h2> confidence high · 1/1 runs · evidence verified · id ef7716588da6 ✗ #email The label "Field 1" does not say what to enter. Evidence: "Field 1" Patch: - <label for="email">Field 1</label> + <label for="email">Email address</label> confidence high · 1/1 runs · evidence verified · id 064fe292df3a WCAG 3.1.2 (AA) — Language of Parts ✗ html > body > main > blockquote:nth-of-type(1) Marked as lang="es", but the text is in Dutch (nl). Evidence: "Prachtige mok, de koffie blijft lang warm. Ik koop er zeker nog een!" Patch: - <blockquote lang="es"> + <blockquote lang="nl"> confidence high · 1/1 runs · evidence verified · id ade39251a2e7 Model calls: 13 new, 0 from cache · 7.1k in / 0.9k out tokens · local model, no API cost Coverage of this run Checked by axe-core (partial): 1.1.1, 1.3.1, 1.3.5, 1.4.3, 2.4.1, 2.4.2, 2.4.4, 3.1.1, 3.1.2, 3.3.2, 4.1.2 Judged with verified evidence: 1.1.1, 2.4.2, 2.4.4, 2.4.6, 3.1.1, 3.1.2 Not checked automatically: 38 of 50 WCAG 2.1 A/AA criteria (--verbose lists them) This report does not declare the page accessible. What was not checked needs manual review and testing with people.
Measured against the W3C’s own test cases
The W3C publishes test cases with the expected result for each accessibility rule. rampa eval runs axe-core alone and axe-core with Rampa on the same pages, for all six criteria.
| Set | Tests | Cases | axe-core, precision / recall | Rampa, precision / recall / F1 |
|---|---|---|---|---|
| 1.1.1, image has a name (ACT 23a2a8) | syntax | 18 | 1.00 / 1.00 | 1.00 / 1.00 / 1.00 |
| 1.1.1, name describes the image (ACT qt1vmo) | meaning | 16 | not defined, nothing flagged / 0.00 | 1.00 / 1.00 / 1.00 |
| 2.4.2, page has a title (ACT 2779a5) | syntax | 12 | 1.00 / 1.00 | 0.50 / 1.00 / 0.67 |
| 2.4.2, title describes the page (ACT c4a8a4) | meaning | 6 | not defined, nothing flagged / 0.00 | 1.00 / 1.00 / 1.00 |
| 2.4.4, link has a name (ACT c487ae) | syntax | 28 | 1.00 / 1.00 | 0.92 / 1.00 / 0.96 |
| 2.4.4, link in context is descriptive (ACT 5effbb) | meaning | 18 | not defined, nothing flagged / 0.00 | 0.67 / 1.00 / 0.80 |
| 2.4.6, heading is descriptive (ACT b49b2e) | meaning | 12 | not defined, nothing flagged / 0.00 | 0.80 / 1.00 / 0.89 |
| 2.4.6, field label is descriptive (ACT cc0f0a) | meaning | 16 | not defined, nothing flagged / 0.00 | 1.00 / 0.83 / 0.91 |
| 3.1.1, page has a valid lang (ACT b5c3f8, bf051a) | syntax | 11 | 1.00 / 1.00 | 1.00 / 1.00 / 1.00 |
| 3.1.1, lang matches the page (ACT ucwvc8) | meaning | 14 | 0.00 / 0.00 | 0.50 / 1.00 / 0.67 |
| 3.1.2, valid lang on parts (ACT de46e4) | syntax | 19 | 1.00 / 1.00 | 1.00 / 1.00 / 1.00 |
| 3.1.2, lang matches the text (ACT off6ek) | meaning | 13 | not defined, nothing flagged / 0.00 | 0.80 / 1.00 / 0.89 |
Break a page on purpose and see who notices
Each pair is a passing test page and a copy broken on purpose. An alt becomes img-1, the title becomes “Welcome”, headings become “Part 1” and labels “Entry 1”, a lang becomes another valid language. None of those words appear in the prompts. A checker that gives both versions the same verdict is not judging.
The method comes from López-Gil and Pereira (2025).
| Broken on purpose | Pairs | axe-core, told apart | Rampa, told apart |
|---|---|---|---|
| Image alt | 2 | 0 | 2 |
| Page title | 3 | 0 | 3 |
| Headings and labels | 9 | 0 | 8 |
| Page lang | 4 | 0 | 4 |
| lang on parts | 5 | 0 | 4 |
| All pairs | 23 | 0 | 21 |
On a local GPU the evaluation costs nothing. With gpt-6-luna, the 36 judgments of 1.1.1 and 3.1.2 took 29 seconds and about half a US cent, and two runs with fresh calls gave the same verdict on every case.
The samples are small and each set ran once. The prompts for the four new criteria were tuned after reading their errors on these cases, so those numbers are optimistic until fresh pages confirm them. Two low precisions are scoring: 2779a5 only asks for a title to exist, and its pages are titled “Title of the page.”; ucwvc8 counts pages without a valid lang, which axe-core fails, as false positives.
Runs on your model
Pick the model per criterion with rampa eval, not by reputation. Every answer is cached, so the same input never calls a model twice.
On your machine
Nothing leaves your computer, and there is no API cost.
- Ollama
- LM Studio
- Any OpenAI-compatible server, such as vLLM or llama.cpp
With a key
Only what rules could not decide leaves your machine.
- OpenAI
- Anthropic
- Google Gemini
- Azure OpenAI
- Amazon Bedrock
- Google Vertex AI
- Mistral AI
- xAI
- Groq
- DeepSeek
- Together AI
- Fireworks AI
- Cerebras
- OpenRouter
- Vercel AI Gateway
Choose with a flag, an environment variable or the config file:
$ rampa check https://example.com --model ollama:gemma4:12b
$ RAMPA_MODEL=openai:gpt-6-luna rampa check https://example.com
$ rampa modelsWhat Rampa won’t do
- Call a page accessible.
- Every report lists what was checked, what was judged and what nobody checked. Passing a check is not the same as being accessible.
- Show a claim it can’t back up.
- A model’s claim reaches you only if the node it cites exists and the text it quotes is in it.
- Bury you in noise.
- False positives are what make teams switch a checker off, so confidence thresholds, waivers and voting across runs are part of the design.
- Touch your production pages.
- It is not an overlay. It runs where you build: on your machine and in CI.
- Phone home.
- There is no telemetry. With a local model, nothing leaves your computer.
- Tie you to one vendor.
- Use any provider, and let
rampa evalmeasure which model to trust for each criterion. - Replace people.
- Automated checks cover part of WCAG. Audits and testing with disabled people cover the rest.
The web today, any screen next
Criteria read a normalized snapshot, never the DOM. An Android or iOS screen becomes the same format, so the check for WCAG 1.1.1 works the same on alt, contentDescription or accessibilityLabel. Collectors for Android and iOS are on the roadmap, and a snapshot exported by your app’s tests can be checked today.
- Web
altAvailable - Android
contentDescriptionOn the roadmap - iOS
accessibilityLabelOn the roadmap
Questions about automated accessibility testing
Is axe-core enough to meet WCAG?
No automated tool is. axe-core reliably finds what markup can prove: a missing alt, an empty link, low contrast. It cannot tell whether an alt describes the image or a heading describes its section. Rampa keeps axe-core for what rules can decide and judges part of the rest; most WCAG criteria still need people.
Can AI check web accessibility?
In part. A language model can judge meaning, such as whether a link text says where it goes, but it can also invent problems. Rampa asks about one criterion at a time with the smallest context, and drops every claim whose evidence is not on the page. rampa eval measures the result against the W3C ACT test cases.
Which WCAG criteria does Rampa check?
axe-core covers parts of about a dozen WCAG 2.1 A and AA criteria. Rampa also judges six with evidence: 1.1.1 Non-text Content, 2.4.2 Page Titled, 2.4.4 Link Purpose (In Context), 2.4.6 Headings and Labels, 3.1.1 Language of Page and 3.1.2 Language of Parts.
Does my page leave my machine?
Not with a local model through Ollama. With a cloud provider, only what rules could not decide is sent, one element and its context per question. There is no telemetry.
How is Rampa different from an accessibility overlay?
An overlay changes a live page for its visitors and leaves the source as it was. Rampa runs where you build, in a terminal or in CI, and reports each finding with a patch for a person to review.
Check one page
Rampa is not on npm yet. Clone it and run it from source with Node.js 22.12 or newer and Chrome or Edge.
With Ollama running, Rampa picks a local model by itself. Otherwise put a key such as OPENAI_API_KEY in a .env file. node dist/cli.mjs doctor tells you what is missing.
git clone https://github.com/guilhermebsantiago/rampa-cli.git
cd rampa-cli
pnpm install && pnpm build
node dist/cli.mjs check https://example.com