What 3.1.1 requires

The default human language of each web page can be programmatically determined.

Success criterion 3.1.1 Language of Page (Level A), quoted from Web Content Accessibility Guidelines (WCAG) 2.1, W3C Recommendation 06 May 2025. Copyright © 2020-2025 World Wide Web Consortium. Used under the W3C Document License.

With the right lang on the html element, screen readers load the right pronunciation rules, browsers display characters and scripts correctly, and media players show captions properly. With the wrong one, a screen reader reads a page in Spanish with English rules.

The default language is the one used most. Understanding 3.1.1 adds that when several languages are used equally, the first one used is the default. Passages in other languages are 3.1.2.

The value is a BCP 47 language tag, such as en, es or pt-BR. A common way to get it wrong is a page translated from a template that kept the template’s lang.

ACT rules for 3.1.1

Common failures

Under each example: what axe-core and Rampa reported on the markup before the fix, in a run with Gemma 4 12B on 8 October 2026.

No lang at all

Without lang, a screen reader reads the page with whatever voice the device is set to, whatever language the text is in. Here rules decide: axe-core fails it, and Rampa reports that without asking a model.

Before, fails 3.1.1

<html>

After, passes

<html lang="en">
axe-core

Fails: html-has-lang.

Rampa
✗ html
  <html> element must have a lang attribute
  high · rule html-has-lang

A value that is not a language tag

lang takes a language tag such as en, not the name of a language. axe-core fails this too.

Before, fails 3.1.1

<html lang="english">

After, passes

<html lang="en">
axe-core

Fails: html-lang-valid.

Rampa
✗ html
  <html> element must have a valid value for the lang attribute
  high · rule html-lang-valid

A valid tag for the wrong language

The Spanish page was built from the English template and kept lang="en". The tag is valid, so every rule passes, and a screen reader reads Spanish with English pronunciation.

Before, fails 3.1.1

<html lang="en">
…
<h1>Horario de atención</h1>
<p>De lunes a viernes, de 7 a 18 h.
   Los sábados, de 8 a 13 h.</p>

After, passes

<html lang="es">
axe-core

Passes: html-has-lang and html-lang-valid find a valid tag.

Rampa
✗ html
  The page is marked lang="en", but most of its text is in Spanish (es).
  Evidence: "Horario de atención De lunes a viernes, de 7 a 18 h. Los sábados, de 8 a 13 h."
  Patch:
    - <html lang="en">
    + <html lang="es">
  confidence high · 1/1 runs · evidence verified · id 804b6c733e31

What axe-core checks

axe-core 4.14.0 maps three rules to 3.1.1: html-has-lang fails a page with no lang, html-lang-valid one whose lang is not a valid tag, and html-xml-lang-mismatch one whose lang and xml:lang disagree. Rampa reports their failures as they are, and a page that failed any of them never goes to the model.

axe-core 4.14.0 rules for 3.1.1
RuleWhat it checksMapped toIn a Rampa run
html-has-lang, rule page at Deque University<html> element must have a lang attribute3.1.1runs
html-lang-valid, rule page at Deque University<html> element must have a valid value for the lang attribute3.1.1runs
html-xml-lang-mismatch, rule page at Deque UniversityHTML elements with lang and xml:lang must have the same base language3.1.1runs

None of them reads the text. lang="en" on a page written in Spanish passes all three.

Each rule links to its page at Deque University, which makes axe-core.

What Rampa judges, and how

What goes to the model

The page, once: when its root element declares a valid lang, none of the three rules failed, and at least 12 letters of text inherit that language.

What the model sees

  • The declared language.
  • The text that inherits it, in reading order, up to 1,500 characters. Passages that declare their own lang are left out, because they are 3.1.2.

What it must answer

A verdict, the language most of the text is in as a BCP 47 primary tag, a short excerpt copied from the text as evidence, and a confidence.

What drops a claim

  • The excerpt is not in the page’s text, is shorter than two letters, or is longer than 400 characters.
  • The detected language is not a valid BCP 47 tag.
  • A fail detects the declared language, or a pass detects another one.

The patch

The root element’s lang becomes the detected primary tag, such as es.

For every criterion

  • Page content reaches the model marked as data, and the prompt tells it to ignore any instruction inside.
  • Answers are cached by a hash of the prompt, the image, the model and its settings, so the same input never calls a model twice.
  • --runs 3 asks three times and keeps the majority; when runs disagree, confidence drops. Findings below --min-confidence (medium by default) are hidden, and --verbose lists them.
  • Claims dropped by verification are counted in every report, never shown as findings.

How Rampa was measured on it

W3C ACT test cases for 3.1.1: precision, recall and F1, Gemma 4 12B on a local GPU
SetTestsCasesaxe-core, precision / recallRampa, precision / recall / F1
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

Corrupted pairs: Rampa told 4 of 4 apart (page lang), axe-core 0. A pair is a passing test page and a copy broken on purpose; a checker that gives both the same verdict is not judging.

Run on 9 October 2026: axe-core 4.14.0, Gemma 4 12B on a local GPU, reasoning off, one run, ACT test cases a9a1483e. Small samples and one run: read the numbers as a working pipeline, not as a result.

Read with these caveats

  • The judgment for 3.1.1 came with 2.4.2, 2.4.4 and 2.4.6 on 2026-10-08, and their prompts were tuned after reading their errors on these cases, so the numbers are optimistic until fresh pages confirm them.
  • The low precision on ucwvc8 is scoring, not judgment. Every passed and failed example is right; the false positives are pages the rule calls inapplicable, four of which axe-core fails for a missing or invalid lang.

Method and error analysis in the README

Limitations

  • Only a valid lang is judged. A missing or invalid one is axe-core’s finding.
  • The model reads up to 1,500 characters of the text that inherits the page language, so a page whose start differs from the rest can be misjudged.
  • Pages with fewer than 12 letters of text are skipped.
  • When two languages are used evenly, WCAG makes the first one used the default; the model may answer that it cannot tell instead.
  • The patch proposes the primary tag only, such as pt; add a region subtag yourself if you want one.
  • Only web pages: snapshots from apps are not judged for 3.1.1.

Questions about 3.1.1

Which language should lang name on a page with two languages?

The one used most. Understanding 3.1.1 says that when two are used equally, the first one used is the default. Passages in the other language get their own lang, which is 3.1.2.

Is lang="pt" enough, or does it need pt-BR?

3.1.1 asks that the language can be determined by software, and a primary tag such as pt does that. A region subtag such as pt-BR adds information. Rampa’s patch proposes only the primary tag.

Does axe-core check that lang matches the content?

No. html-has-lang checks that the attribute exists and html-lang-valid that its value is a valid tag. lang="en" on a page in Spanish passes both.

Check your pages

Rampa is not on npm yet. Clone it and run it from source with Node.js 22.12 or newer and Chrome or Edge. --criteria 3.1.1 judges only this criterion; leave it out to run the eight that run by default.

With Ollama running, Rampa picks a local model by itself; node dist/cli.mjs doctor says 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 --criteria 3.1.1

Checked against rampa-cli (commit 0459809) and the W3C sources on 9 October 2026.