auditmysitev2.0.2

Invalid headers reference in a table cell

tables/headers-attr-invalid

WCAG success criterion
1.3.1 (level A)
Outcome class
Automatic. An automated check that can report a confirmed violation.
Area
Accessibility, Structure & Semantics
Default severity
high
axe-core rule
td-headers-attr

Why we flag this

Screen readers cannot associate the cell with its correct column/row header, or none at all.

headers attribute does not reference a valid th cell in the same table.

What the rule checks

Scope. Every data or header cell (<td>, <th>, cell, gridcell, columnheader, rowheader) with a headers attribute, in a rendered table.

Expectation. H43/HTML: headers must list ids of header cells of the same table. Fails at the cell when the attribute is empty, names an id that does not exist, names a non-header cell, or a header cell of another table. A cell of a table that is not rendered (hidden) presents no association to anyone and is not judged in that state (WCAG conformance applies to presented content).

Source. WCAG 2.2 SC 1.3.1 Info and Relationships (H51, H39, H63, H43, F91, F92); HTML §4.9 tabular data; WAI-ARIA 1.2 table/grid/row/cell roles

Detection: shared; needs dom, computed_style.

Failing example

The check reports a violation here. Corpus case content_tables_broken, element td#cell-missing-ref. The page may contain further findings for other rules.

content_tables_broken.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Library statistics with broken tables</title>
    <style>
        nav a, .skip-link { display: inline-block; min-width: 44px; min-height: 44px; }
    </style>
</head>
<body>
    <a class="skip-link" href="#main">Skip to main content</a>
    <header>
        <p>City Library</p>
        <nav aria-label="Main"><a href="/" aria-current="page">Home</a> <a href="/catalogue">Catalogue</a> <a href="/visit">Visit</a></nav>
    </header>
    <main id="main">
        <h1>Library statistics</h1>
        <h2>Loans</h2>
        <table id="loans">
            <tr><td>January</td><td>1,204</td></tr>
            <tr><td>February</td><td>1,180</td></tr>
        </table>
        <h2>Layout grid</h2>
        <table id="layout-with-headers" role="presentation">
            <tr><th id="layout-head">Column</th></tr>
            <tr><td>Left text</td></tr>
        </table>
        <h2>Upcoming events</h2>
        <table id="events" aria-label="Upcoming events">
            <thead><tr><th id="events-date">Date</th><th id="events-title">Event</th></tr></thead>
        </table>
        <h2>Budget</h2>
        <table id="budget" aria-label="Budget">
            <tr><th id="budget-item">Item</th><th id="budget-cost">Cost</th></tr>
            <tr><td id="cell-missing-ref" headers="budget-nothing">Paper</td><td id="cell-empty-ref" headers="">5</td></tr>
            <tr><td id="cell-to-cell" headers="cell-missing-ref">Ink</td><td id="cell-other-table" headers="opening-day">8</td></tr>
        </table>
        <table id="opening" aria-label="Opening">
            <tr><th id="opening-day">Day</th><td>Monday</td></tr>
        </table>
        <h2>Branch overview</h2>
        <div role="table" id="aria-branches">
            <div role="row"><div role="cell">North</div><div role="cell">Open</div></div>
        </div>
        <div role="grid" id="aria-headers-only" aria-label="Seat map">
            <div role="row"><div role="columnheader" id="aria-seat-head">Seat</div></div>
        </div>
        <h2>Archive</h2>
        <table id="hidden-table" hidden><tr><td>Old</td><td id="hidden-cell" headers="nothing-here">1</td></tr></table>
        <div aria-hidden="true"><table id="decorative-table"><tr><td>Pattern</td></tr></table></div>
        <h2>Widgets</h2>
        <stat-table></stat-table>
        <iframe id="frame-table" title="Branch statistics" style="width:400px;height:120px;border:0" srcdoc="<!DOCTYPE html><html lang=&quot;en&quot;><head><title>Branch statistics</title></head><body><table id=&quot;frame-stats&quot;><tr><td>Visitors</td><td>120</td></tr></table></body></html>"></iframe>
        <div id="late-table"></div>
        <script>
            customElements.define('stat-table', class extends HTMLElement {
                connectedCallback() {
                    this.attachShadow({ mode: 'open' }).innerHTML =
                        '<table id="shadow-stats"><tr><td>Visitors</td><td>310</td></tr></table>';
                }
            });
            document.getElementById('late-table').innerHTML =
                '<table id="late-stats"><tr><td>Members</td><td>4,800</td></tr></table>';
        </script>
    </main>
    <footer><p>City Library, 1 Market Square</p></footer>
</body>
</html>

Passing example

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

content_tables_ok.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Library statistics</title>
    <style>
        nav a, .skip-link { display: inline-block; min-width: 44px; min-height: 44px; }
    </style>
</head>
<body>
    <a class="skip-link" href="#main">Skip to main content</a>
    <header>
        <p>City Library</p>
        <nav aria-label="Main"><a href="/" aria-current="page">Home</a> <a href="/catalogue">Catalogue</a> <a href="/visit">Visit</a></nav>
    </header>
    <main id="main">
        <h1>Library statistics</h1>
        <table id="prices">
            <caption>Copy and print prices</caption>
            <thead><tr><th scope="col" id="price-item">Item</th><th scope="col" id="price-cost">Cost</th></tr></thead>
            <tbody>
                <tr><td headers="price-item">Black and white page</td><td headers="price-cost">0.10</td></tr>
                <tr><td headers="price-item">Colour page</td><td headers="price-item price-cost">0.50</td></tr>
            </tbody>
        </table>
        <h2 id="hours-heading">Opening hours</h2>
        <table id="hours" aria-labelledby="hours-heading">
            <tr><th scope="row">Monday</th><td>9:00 to 18:00</td></tr>
            <tr><th scope="row">Sunday</th><td></td></tr>
        </table>
        <table id="minimal" aria-label="Late returns">
            <tr><th id="late-head">Late returns this week</th><td></td></tr>
        </table>
        <table id="layout" role="presentation">
            <tr><td>Address: 1 Market Square</td><td>Phone: 555 0100</td></tr>
        </table>
        <div role="table" id="aria-branches" aria-label="Branches">
            <div role="row"><div role="columnheader">Branch</div><div role="columnheader">Status</div></div>
            <div role="row"><div role="cell">North</div><div role="cell">Open</div></div>
        </div>
        <stat-table></stat-table>
        <script>
            customElements.define('stat-table', class extends HTMLElement {
                connectedCallback() {
                    this.attachShadow({ mode: 'open' }).innerHTML =
                        '<table id="shadow-stats"><caption>Visitors</caption>' +
                        '<tr><th scope="col" id="shadow-day">Day</th><th scope="col">Visitors</th></tr>' +
                        '<tr><td headers="shadow-day">Monday</td><td>310</td></tr></table>';
                }
            });
        </script>
    </main>
    <footer><p>City Library, 1 Market Square</p></footer>
</body>
</html>

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