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.

Illustration: a flight of stairs at a building entrance turns into a ramp as the page scrolls. The ramp rises 1 unit for every 12 it runs, the steepest slope allowed for a wheelchair ramp.

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.

Illustration of a brown dog with a red collar and a yellow tag.<img src="dog.svg" alt="img-1">
An example page in Portuguese publishes this illustration with 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-1

Suggested 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 pageaxe-coreRampa
WCAG 2.4.2<title>Untitled document</title>
passesThe page title “Untitled document” says nothing about this page.
WCAG 2.4.4<a href="shipping.html">Click here</a>
passesThe link text “Click here” does not tell where the link goes, and nothing around it does.
WCAG 2.4.6<h2>Section 2</h2>
passesThe heading “Section 2” says nothing about the content under it.
WCAG 2.4.6<label for="email">Field 1</label>
passesThe label “Field 1” does not say what to enter.
WCAG 3.1.1<html lang="en">on a page in Portuguese
passesThe 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.

  1. Collect

    The page becomes a normalized accessibility snapshot: roles, names, languages, states, and every image as it was rendered.

  2. Rules first

    axe-core runs on the page. Whatever its rules can decide goes straight to the report, with no model involved.

  3. 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.

  4. 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.

  5. Report the coverage

    Findings come with a patch, and the report lists what nobody checked. It never calls a page accessible.

Of the 50 WCAG 2.1 success criteria at levels A and AA, a run on the store example checks 12, judges 6 of them with evidence, and leaves 38 to people.

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.

Precision, recall and F1 per set, with Gemma 4 12B on a local GPU.
SetTestsCasesaxe-core, precision / recallRampa, precision / recall / F1
1.1.1, image has a name (ACT 23a2a8)syntax181.00 / 1.001.00 / 1.00 / 1.00
1.1.1, name describes the image (ACT qt1vmo)meaning16not defined, nothing flagged / 0.001.00 / 1.00 / 1.00
2.4.2, page has a title (ACT 2779a5)syntax121.00 / 1.000.50 / 1.00 / 0.67
2.4.2, title describes the page (ACT c4a8a4)meaning6not defined, nothing flagged / 0.001.00 / 1.00 / 1.00
2.4.4, link has a name (ACT c487ae)syntax281.00 / 1.000.92 / 1.00 / 0.96
2.4.4, link in context is descriptive (ACT 5effbb)meaning18not defined, nothing flagged / 0.000.67 / 1.00 / 0.80
2.4.6, heading is descriptive (ACT b49b2e)meaning12not defined, nothing flagged / 0.000.80 / 1.00 / 0.89
2.4.6, field label is descriptive (ACT cc0f0a)meaning16not defined, nothing flagged / 0.001.00 / 0.83 / 0.91
3.1.1, page has a valid lang (ACT b5c3f8, bf051a)syntax111.00 / 1.001.00 / 1.00 / 1.00
3.1.1, lang matches the page (ACT ucwvc8)meaning140.00 / 0.000.50 / 1.00 / 0.67
3.1.2, valid lang on parts (ACT de46e4)syntax191.00 / 1.001.00 / 1.00 / 1.00
3.1.2, lang matches the text (ACT off6ek)meaning13not defined, nothing flagged / 0.000.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).

Pairs told apart, by what was broken. A filled square is a pair whose two versions got different verdicts.
Broken on purposePairsaxe-core, told apartRampa, told apart
Image alt202
Page title303
Headings and labels908
Page lang404
lang on parts504
All pairs23021

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.

Method and error analysis in the README

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 models

What 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 eval measure 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.

  • WebaltAvailable
  • AndroidcontentDescriptionOn the roadmap
  • iOSaccessibilityLabelOn 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.

Open the repository
git clone https://github.com/guilhermebsantiago/rampa-cli.git
cd rampa-cli
pnpm install && pnpm build
node dist/cli.mjs check https://example.com