auditmysitev2.0.2

Layout table with header cells

tables/presentational-with-headers

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
medium
axe-core rule
tables/presentational-with-headers

Why we flag this

Screen reader users receive contradictory signals about whether this is a data table.

role="presentation"/"none" on a table with <th>.

What the rule checks

Scope. Every rendered table marked role="presentation" or role="none".

Expectation. A table marked presentational but holding header cells (<th>, columnheader, rowheader) contradicts itself: the role removes the table semantics the header cells announce (F92 reasoning). Confirmed violation at the table. A presentational layout table without header cells passes.

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 table#layout-with-headers. 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.