auditmysitev2.0.2

Missing language declaration

html-has-lang

WCAG success criterion
3.1.1 (level A)
Outcome class
Automatic. An automated check that can report a confirmed violation.
Area
Accessibility, Language & Clarity
Default severity
high
axe-core rule
document/lang-missing

Why we flag this

Screen readers read aloud with incorrect pronunciation.

Missing lang attribute.

What the rule checks

Scope. Rendered same-origin iframe documents (also in open shadow roots): the frame is not display:none/visibility:hidden, not aria-hidden, not role=none/presentation, larger than 1x1 px.

Expectation. Fails for every applicable frame document whose root html element has no non-empty lang: lang does not inherit across the document boundary, so the embedded content's language is not programmatically determinable although it is part of the page. A frame document with lang passes. Collapsed or hidden frames present nothing and do not apply. Cross-origin frames cannot be read (frame-tested). One finding per frame.

Source. WCAG 2.2 SC 3.1.1 Language of Page; H57; HTML §3.2.6.2 (lang is inherited within a document only)

Detection: page; needs dom, frames, computed_style, geometry.

Failing example

The check reports a violation here. Corpus case structure_ids_frames, element iframe#reviews-frame [frame] html. The page may contain further findings for other rules.

structure_ids_frames.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Store finder – Acme Hardware</title>
    <style>
        body { font: 16px/1.5 sans-serif; margin: 0 16px; }
        a { display: inline-block; min-width: 44px; min-height: 44px; line-height: 44px; }
        input, button { min-width: 44px; min-height: 44px; }
        iframe { border: 1px solid #555; }
        .collapsed { display: none; }
    </style>
</head>
<body>
    <!-- Embedded widgets and help texts (#783). -->
    <a href="#main" class="skip-link">Skip to content</a>
    <header>
        <p>Acme Hardware</p>
    </header>
    <nav aria-label="Main">
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/stores/">Stores</a></li>
        </ul>
    </nav>
    <main id="main">
        <h1>Store finder</h1>

        <h2>Map</h2>
        <!-- Frames without an accessible name: no title at all, and an empty title. -->
        <iframe id="map-frame" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>Map</title></head><body><p>Map of Berlin</p></body></html>"></iframe>
        <iframe id="weather-frame" title="" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>Weather</title></head><body><p>Sunny, 21 degrees</p></body></html>"></iframe>
        <!-- Named by title and by aria-label: fine. The first embedded document
             has a default title, which titles the frame document, not this page;
             the second embedded document has no lang. -->
        <iframe id="hours-frame" title="Opening hours" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>Untitled</title></head><body><p>Open 8 to 18</p></body></html>"></iframe>
        <iframe id="reviews-frame" aria-label="Customer reviews" width="300" height="150"
            srcdoc="<!DOCTYPE html><html><head><title>Reviews</title></head><body><p>Four out of five stars</p></body></html>"></iframe>
        <!-- Presentational, collapsed and aria-hidden frames are not exposed. -->
        <iframe id="tracking-frame" role="presentation" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>t</title></head><body></body></html>"></iframe>
        <iframe id="collapsed-frame" class="collapsed" width="300" height="150"
            srcdoc="<!DOCTYPE html><html><head><title>c</title></head><body><p>Hidden</p></body></html>"></iframe>
        <iframe id="hidden-frame" aria-hidden="true" tabindex="-1" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>h</title></head><body></body></html>"></iframe>

        <h2>Find a store</h2>
        <form action="/stores" method="get">
            <!-- The hint id occurs twice and is referenced: ambiguous. -->
            <label for="postcode">Postcode</label>
            <input id="postcode" name="postcode" type="text" autocomplete="postal-code" aria-describedby="postcode-hint">
            <p id="postcode-hint">Five digits, for example 10115.</p>
            <p id="postcode-hint">We only use it to sort the list.</p>
            <!-- Duplicated but referenced by nothing: no relationship breaks. -->
            <p id="promo-text">Free parking at every store.</p>
            <p id="promo-text">Gift cards available.</p>
            <button type="submit">Search</button>
        </form>

        <h2>Store card</h2>
        <!-- The component reuses the id inside its own shadow root; IDREFs
             resolve within their own tree, so neither reference is ambiguous. -->
        <store-card></store-card>
        <p id="card-note">Prices may vary by store.</p>
        <script>
            const card = document.querySelector('store-card').attachShadow({ mode: 'open' });
            // The card also has an empty heading and an untitled frame inside its
            // shadow root.
            card.innerHTML = '<h3 id="card-heading"></h3><p id="card-note">Mitte branch</p><p aria-describedby="card-note">Open Sundays</p>' +
                '<iframe id="card-frame" width="300" height="150" srcdoc="<!DOCTYPE html><html><head><title>Card</title></head><body><p>Route</p></body></html>"></iframe>';
        </script>
        <p aria-describedby="card-note">All stores</p>
    </main>
    <footer>
        <p>Acme Hardware, Berlin</p>
    </footer>
</body>
</html>

Passing example

The check passes here and reports nothing. Corpus case structure_bypass_missing. The page may contain further findings for other rules.

structure_bypass_missing.htmlHTML
<!DOCTYPE html>
<html lang="en_US">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body { font: 16px/1.5 sans-serif; margin: 0 16px; }
        a { display: inline-block; min-width: 44px; min-height: 44px; line-height: 44px; }
        input, button { min-width: 44px; min-height: 44px; }
    </style>
</head>
<body>
    <!-- An old intranet page (#783): no <title> at all, a locale-style lang
         value ("en_US" uses an underscore, which BCP 47 does not allow, so
         the primary subtag is not "en"), and a repeated navigation block with
         no way to bypass it: no heading, no main landmark, no skip link. -->
    <nav>
        <a href="/">Intranet home</a>
        <a href="/staff/">Staff directory</a>
        <a href="/forms/">Request forms</a>
        <a href="/help/">IT help desk</a>
    </nav>
    <!-- The only heading is hidden, so it bypasses nothing. -->
    <h1 hidden>Intranet</h1>
    <div class="content">
        <p>Holiday requests for next year are due by the end of the month.</p>
        <p>Use the staff portal to submit them.</p>
        <!-- An embedded calendar with its own title: it titles the frame
             document, not this page. -->
        <iframe title="Staff calendar" width="300" height="150"
            srcdoc="<!DOCTYPE html><html lang='en'><head><title>Staff calendar</title></head><body><p>No events today</p></body></html>"></iframe>
    </div>
</body>
</html>

This page is generated from the rule catalogue and the rule contracts; see therule overview for how the outcome classes are derived.