( Understandable / WCAG 3.1.1 )

lang and xml:lang do not match

ModerateLevel AWCAG 3.1.1 — Language of Page

What is this issue?

The <html> element carries both a lang attribute and an xml:lang attribute, and their base language values disagree: for example, lang="en" alongside xml:lang="fr". Both attributes exist to declare the document’s language; xml:lang is the XML-namespace equivalent of lang, relevant only when a document is parsed as XML (including XHTML served with an XML content type).

This differs from a straightforwardly missing or invalid lang value: here, the page has made two separate declarations that actively contradict each other, rather than failing to make one declaration correctly.

Why does this matter?

When lang and xml:lang disagree, there’s no single behavior every tool falls back to consistently. Some browsers and screen readers give lang precedence; others, particularly when a document is parsed under XML rules, give xml:lang precedence. The result isn’t one predictable wrong pronunciation. It’s an inconsistent one that can differ from user to user depending entirely on which browser and screen reader combination they happen to run.

That unpredictability makes the defect harder to catch during manual testing than a flat-out missing attribute: a developer testing with one browser and screen reader pairing might see the page pronounced correctly, while a user on a different combination hits the exact mismatch the conflicting attributes created.

Who is affected?

  • Screen reader users: get inconsistent pronunciation behavior depending on which browser and screen reader combination they use, since different tools resolve a lang/xml:lang conflict differently rather than failing the same way for everyone.

What users experience

Priya uses JAWS with Chrome to browse a company’s investor relations page, which was migrated from an older XHTML template years ago. The <html> element has lang="en" but a leftover xml:lang="de" from a German-market variant the template was originally cloned from. Chrome and JAWS resolve the conflict in favor of xml:lang for this particular parsing path, so JAWS reads the English-language financial report using German pronunciation rules. A colleague testing the same page in Firefox with NVDA hears it read correctly in English, since that combination resolves the conflict the other way, making the bug look intermittent and hard to reproduce during QA.

How do I fix it?

Set lang and xml:lang to the identical language code whenever both are present. This works because it removes the conflict entirely: every tool now agrees on the answer regardless of which attribute it happens to prioritize, so there’s no longer a wrong path to fall into.

For most modern sites, the more durable fix is to remove xml:lang entirely rather than keep two attributes synchronized indefinitely. xml:lang only matters when a document is served with an XML content type; a standard HTML5 page served as text/html gets no benefit from it and only risks drifting out of sync again during a future template or localization change.

Code Examples

Before
<html lang="en" xml:lang="fr">
After
<!-- Option 1: keep both, synchronized -->
<html lang="en" xml:lang="en">

<!-- Option 2: drop xml:lang if the page is served as text/html -->
<html lang="en">

Synchronizing both values resolves the immediate conflict for every tool reading the page. Removing xml:lang entirely goes further, eliminating the attribute that had no purpose on a text/html page in the first place and can’t drift out of sync again if it’s no longer there to drift.

Common Mistakes

Mistake: “Having both lang and xml:lang set is more thorough, so it’s better than just one.” Two attributes are only “more thorough” if they’re kept in sync; the moment they diverge, having both is strictly worse than having one, since it introduces an ambiguity that a single correct lang attribute never would have created.

Mistake: “This only matters for XHTML sites, so a modern HTML5 page can ignore a mismatch.” The mismatch still gets read by some browser and screen reader combinations regardless of the page’s actual document type declaration, since not every tool checks the served content type before deciding which attribute to trust. Treat any conflicting pair as a live defect, not a legacy-format-only concern.

Mistake: “As long as the base html-has-lang and html-lang-valid checks pass, the page’s language declaration is fine.” Both of those checks can pass independently while this mismatch still exists: lang alone can be present and perfectly valid, and xml:lang alone can also be present and perfectly valid, while still disagreeing with each other. This is a distinct condition that needs its own check.

How RedFlag Detects This

Automated: axe-core rule, runs on every scan. RedFlag calls axe-core’s html-xml-lang-mismatch rule as part of every scan, restricted to the WCAG 2.0/2.1/2.2 A and AA rule set. The rule reads the <html> element’s lang and xml:lang attribute values, when both are present, and compares their base (primary) language subtags case-insensitively, flagging a violation when they don’t match.

False negative: the rule only compares base language subtags, not region subtags: lang="en-US" against xml:lang="en-GB" both declare English and pass this check, even though the regional variants differ, since axe-core’s comparison intentionally stops at the primary subtag. False positive: none typical for this check, since comparing two present attribute values for a base-language match is a binary condition axe-core evaluates reliably. Manual step: when only one of the two attributes is present rather than both, this rule doesn’t apply at all; confirm separately with html-has-lang and html-lang-valid that the single attribute present is itself correct.

Manual Testing

  1. Open the page in Chrome or Firefox and inspect the <html> element’s attributes in developer tools.
  2. Check whether both lang and xml:lang are present, and if so, compare their values.
  3. If both are present, load the page with two different screen reader and browser combinations, such as NVDA with Firefox and JAWS with Chrome, and listen to whether the pronunciation is consistent between them.
  4. If the two attributes name different base languages, or the pronunciation differs noticeably between screen reader and browser combinations, the check fails.

3.1.1 Language of Page: The default human language of each web page must be programmatically determinable. A page with two conflicting language declarations doesn’t have one reliably determinable answer, which is why an internal mismatch is still a 3.1.1 failure even though a value is technically present in both attributes.

Page language is not set covers the base case this rule builds on: no declared language at all, rather than two declarations that disagree with each other.

lang attribute has an invalid value covers a single invalid value on either attribute; this rule assumes both attributes individually contain something that looks like a valid code, but checks whether the two values agree with each other.

lang attribute on an element has an invalid value applies a similar validity check further down the page, on individual elements rather than the page-level <html> declaration this rule and its siblings cover.

References

Frequently asked questions

Do modern HTML pages still need xml:lang at all?

Only if the document is served as XHTML or another XML content type. A regular HTML5 page served as text/html does not need xml:lang at all: lang alone is sufficient, and the safest fix for most sites is to remove xml:lang entirely rather than try to keep two attributes in sync.

Which attribute wins when lang and xml:lang disagree, lang or xml:lang?

It depends on the parser and the assistive technology reading the page, which is exactly the problem; there is no single universally-honored winner. Some tools prefer lang, others prefer xml:lang when a document is parsed as XML, so a mismatch produces inconsistent results across different browser and screen reader combinations rather than one predictable wrong answer.

Does the region subtag need to match too, or just the base language?

Just the base language subtag needs to match for this specific check: axe-core compares the first part of each value, so lang="en-US" and xml:lang="en-GB" would not trigger this rule, since both declare English. A base-language mismatch, like lang="en" against xml:lang="fr", is what this rule flags.

How does this defect usually get introduced if nobody added xml:lang on purpose?

It is most commonly leftover markup from a template originally built for XHTML, later migrated to HTML5, where lang was added for the new format but the old xml:lang attribute was never removed and drifted out of sync during a later content or locale update.