( Understandable / WCAG 3.1.1 )
Page is missing a language declaration
What is this issue?
The <html> element’s lang attribute is missing entirely, or its value is empty or whitespace-only once trimmed. There’s no declared default language anywhere on the page for the browser or assistive technology to read.
This page documents the identical condition as html-has-lang, but through a different detection path: this check is a custom DOM detector: RedFlag’s own JavaScript logic, distinct from the bundled axe-core engine, built before RedFlag adopted axe-core’s equivalent rule for the same condition. Both checks now run on every scan and always agree with each other, since they test the same underlying fact.
Why does this matter?
Screen readers (software that reads a page’s content and structure out loud, or to a braille display, for people who are blind or have low vision) rely on a page’s declared lang attribute to select which pronunciation engine and voice to use. Without it, the screen reader falls back to whatever language the user’s system is configured for, whether or not that happens to match the actual language of the page.
When that guess is wrong, the result isn’t a minor accent mismatch; it can make an entire page’s speech output unintelligible, since pronunciation rules for one language applied to text written in another frequently don’t produce recognizable words at all. For a screen reader user, that’s the difference between reading a page normally and not being able to use it at all.
Who is affected?
- Screen reader users: hear the page read with the wrong pronunciation engine and voice entirely, ranging from an odd accent to completely unintelligible speech depending on how different the fallback language is from the page’s real one.
- Cognitive disabilities: users who rely on clearly paced, correctly pronounced speech output to follow written content lose comprehension when the underlying pronunciation engine doesn’t match the language actually being read.
What users experience
Elena uses NVDA on Windows, configured with German as her system language, to check the shipping policy on an English-language online store. The store’s <html> element has no lang attribute at all (an oversight in a custom checkout template that bypassed the site’s usual page shell). NVDA falls back to reading the English policy text using German pronunciation rules, producing speech that doesn’t resolve into recognizable English words. She assumes the page is broken rather than realizing it’s simply missing a language declaration, and abandons the checkout flow rather than trying to puzzle through the garbled speech.
How do I fix it?
Add a lang attribute to the <html> element, set to the page’s primary language using a BCP 47 code, most often a two-letter ISO 639-1 code like en, fr, or es. This is the same fix as html-has-lang documents, since both checks detect the identical missing-attribute condition: setting lang correctly resolves both findings simultaneously, not just one of them.
If this defect is showing up on a specific template or route rather than site-wide, check whether that template shares a shared page shell with the rest of the site. A missing lang attribute on only some pages usually means one template bypasses the shared <head>/<html> boilerplate the rest of the site uses.
Code Examples
<html><html lang="en">Adding the attribute gives every screen reader an explicit language to switch its pronunciation engine to, instead of falling back to the user’s own system default; the fix works identically here as it does for the html-has-lang finding, since they’re two views of the same defect.
Common Mistakes
Mistake: “Two rules flagged this, so it must be two separate problems to fix.” This check and html-has-lang detect the exact same condition through two different code paths. RedFlag’s dashboard deduplicates them into one finding for this reason. Fixing the lang attribute once resolves both, since there was only ever one underlying defect.
Mistake: “This is a custom RedFlag rule, so it must be checking something axe-core doesn’t cover.” For this specific rule, that assumption is wrong: the custom check and the axe-core html-has-lang rule test the identical narrow condition (an empty or missing lang attribute on <html>). Not every redflag--prefixed rule ID represents genuinely unique detection logic; some, like this one, predate axe-core adoption and now simply duplicate a native check.
Mistake: “Setting lang to any non-empty value clears this finding, so the exact value doesn’t matter much.” Both this check and html-has-lang only test whether a lang attribute is present and non-empty; they don’t validate that the value is a real language code. Passing this specific check with an invalid value like lang="xyz" still leaves the page mispronounced; use lang attribute has an invalid value to confirm the value itself is correct, not just present.
How RedFlag Detects This
Custom automated: RedFlag’s own DOM detector, runs on every scan. This check lives in extension/content.js’s runCustomRules() function (check 18): it reads document.documentElement.getAttribute("lang"), trims the result, and flags a violation if the trimmed value is empty. This is the entire check: a single presence-and-non-emptiness test against the <html> element, with no validation of the value’s format or accuracy.
False negative: the check confirms some non-empty value is present; it cannot judge whether that value is a real language code or the correct one for the page’s content, so an invalid or wrong lang value passes this specific check silently (see html-lang-valid for that separate condition). False positive: none typical for this check, since testing for an empty attribute after trimming is a binary condition. Manual step: confirm any lang value present is both a valid BCP 47 code and the actually correct language for the page.
This check is a detector twin of html-has-lang, RedFlag’s axe-core rule for the identical condition. Because both fire together whenever the <html> element lacks a lang attribute, RedFlag’s dashboard deduplicates them into a single finding rather than reporting the same defect under two rule IDs.
Manual Testing
- Open the page in Chrome or Firefox and check the
<html>element’slangattribute in developer tools. - Load the page fresh with NVDA or VoiceOver running and listen to the voice and pronunciation used for the first paragraph of visible text.
- If the
langattribute is absent or empty in the DOM inspector, or the announced speech uses an unexpected accent, the check fails.
Related WCAG Success Criteria
3.1.1 Language of Page: The default human language of each web page must be programmatically determinable. A missing lang attribute means the page’s language cannot be programmatically determined at all, which this custom check and its axe-core twin both confirm directly.
Related Issues
Page language is not set is this rule’s detector twin, checking the identical condition via axe-core instead of RedFlag’s custom DOM logic; the two are deduplicated into a single dashboard finding whenever both fire on the same page.
lang attribute has an invalid value covers the next failure mode past this one: a lang attribute that’s present and non-empty but doesn’t hold a real language code, which this custom presence-only check cannot catch.
lang and xml:lang do not match covers a page with two conflicting language declarations rather than a missing one.
HTML lang attribute is not a valid BCP 47 tag documents a related invalid-value concept, but unlike this page, has no automated detector wired up at all.
References
- W3C Understanding 3.1.1: Language of Page
- W3C Technique H57: Using the language attribute on the html element
- MDN: lang global attribute
Frequently asked questions
Why does RedFlag run two separate checks for the same missing lang defect?
This custom check predates RedFlag adopting axe-core for language checks and was kept in the scan engine rather than removed outright. Both it and the newer html-has-lang axe-core rule check the identical condition, so RedFlag deduplicates the two into a single dashboard finding when they fire on the same page rather than reporting the defect twice.
If this check and html-has-lang always agree, does it matter which one a report references?
Not for the fix: both point at the exact same defect and the exact same remedy, adding a lang attribute to the html element. The dashboard shows one deduplicated finding regardless of which internal check technically fired first, so which rule ID appears in a given report has no bearing on what to do about it.
Does this custom check ever catch something html-has-lang misses, or vice versa?
No. Both check the identical, narrow condition (whether documentElement.getAttribute("lang") is empty or missing after trimming whitespace), so their results are always identical on any given page. Neither has broader or narrower coverage than the other.
What value should I set the lang attribute to once I have found it missing?
Use a valid BCP 47 language code matching the page's primary language, most commonly a two-letter ISO 639-1 code such as "en" for English. Setting any value, even a temporarily wrong one, is not a substitute for setting the genuinely correct one, since an incorrect code still causes mispronunciation.