A screen reader decides how to pronounce your page before it reads a single word. It looks at the lang attribute on <html>. Get it wrong and an English page is read with French phonemes, or a German voice mangles your English nav. Get the per-passage tagging wrong and a quoted sentence in another language comes out as gibberish. This is what WCAG 3.1.1 Language of Page (Level A) and 3.1.2 Language of Parts (Level AA) exist to prevent.
For anyone working on a lang attribute multilingual setup, these two criteria are deceptively simple to state and easy to get subtly wrong, especially once a framework, an i18n library, or user-switchable locales enter the picture. This guide covers the exact markup, the valid values, the framework patterns, and the edge cases that automated checkers miss.
What 3.1.1 and 3.1.2 actually require
Two separate success criteria, two scopes:
- 3.1.1 Language of Page (Level A): the default human language of the whole page must be programmatically determinable. In practice this means a valid
langattribute on the<html>element. - 3.1.2 Language of Parts (Level AA): any passage or phrase in a different language from the page default must have its own
langattribute on the wrapping element, unless it's a proper name, a technical term, a word of indeterminate language, or a word that has become part of the surrounding language.
Both sit inside the WCAG 2.2 Level A and AA baseline that the European Accessibility Act enforces from 28 June 2025 via EN 301 549. The same two criteria appear at their respective levels in Section 508 (WCAG 2.0 AA), AODA (WCAG 2.0 AA), and the de-facto ADA standard, so there is no jurisdiction where you can skip them. See our WCAG criteria reference for how they fit the full set.
Setting the page language: html lang done correctly
The value is a BCP 47 language tag, not a country name and not a guess. Use the shortest valid subtag set that's accurate:
<html lang="en">for English where the regional variant doesn't matter.<html lang="en-GB">or<html lang="pt-BR">only when the regional distinction is meaningful (spelling, currency phrasing, voice selection).<html lang="zh-Hans">/<html lang="zh-Hant">use script subtags for Simplified vs Traditional Chinese, which matters more than region here.
Common mistakes: using lang="english" (invalid, must be the code), using lang="en-US" reflexively when plain en is more honest, or leaving the Create-Next-App-style default lang="en" on a page whose content is actually German. The attribute must match the content, not the template.
If your document is XHTML or served as XML, also set xml:lang with the same value. For ordinary HTML5 served as text/html, the lang attribute alone is correct and sufficient.
Marking foreign-language passages (3.1.2)
Wrap any run of text in a different language and tag it. Inline phrases use <span>; block-level quotes use <blockquote>, <p>, or a list item:
Example: an English page quoting French. <p>She ended with a quiet <span lang="fr">je ne sais quoi</span> and left.</p> Without the span, a screen reader pronounces "je ne sais quoi" using English letter-to-sound rules.
What you do NOT tag: proper names ("Volkswagen" on an English page), single borrowed words that are now part of the language ("rendezvous", "croissant"), technical terms, and text in an undetermined language. Over-tagging is not a failure, but tagging every loanword is noise; reserve it for genuine other-language passages a synthesizer would mispronounce.
For content with no spoken form, or unknown content, lang="zxx" (no linguistic content) and lang="und" (undetermined) are the correct BCP 47 escape hatches rather than a wrong tag.
Multilingual site patterns and framework wiring
The hard part is keeping <html lang> in sync with the rendered locale on every route. Patterns by stack:
- Static / templated sites: set
langfrom the build locale so each generated page ships the correct value. A single hardcodedlangin a shared layout is the most common multilingual bug. - React SPAs: the root HTML often renders before the router knows the locale. Update it on locale change with
document.documentElement.lang = localeinside an effect, so the attribute follows client-side navigation and language switches. - Next.js App Router: read the active locale where you render the root
<html>element and pass it as thelangprop per request, rather than baking in a constant. Because this Next.js version's conventions differ from older releases, confirm the current API in node_modules/next/dist/docs before wiring it. - i18n libraries (i18next, FormatJS, etc.): drive the attribute from the same locale state that drives translations, so there is one source of truth and the two can never disagree.
A language switcher itself needs care: the links to other-language versions should carry hreflang for the target, and if a menu lists language names in their own script ("Deutsch", "日本語", "Español"), tag each item with its own lang so the option names are read correctly.
Testing it (and what automation can and can't catch)
- Automated: a scanner reliably flags a missing or empty
<html lang>and invalid tag syntax. Run a free pass with AccessScan to catch those page-level misses across your routes. - What automation can't judge: whether the declared language matches the actual content, or whether an untagged foreign passage should have been tagged. No tool reads meaning, so 3.1.2 needs human review.
- Manual: turn on a screen reader (VoiceOver, NVDA) and listen. A wrong page language or a missing part tag is immediately audible.
Fold these checks into your release process via the accessibility checklist so locale-correct lang becomes a build expectation rather than an afterthought. The combination of automated page-level scanning and a short manual pass on translated content covers both criteria without much overhead.