Improper Table Structure | Blue Frog Docs

Improper Table Structure

Build accessible data tables with proper headers, captions, and semantic markup

Improper Table Structure

What This Means

Accessible table structure uses proper HTML table elements with headers, captions, and scope attributes to help screen readers understand the relationship between data cells. When tables lack proper structure, screen reader users can't navigate data, understand column/row relationships, or determine what each cell represents, making data tables completely unusable for people with visual impairments.

How Screen Readers Navigate Tables

What Screen Readers Need:

  1. Caption - What is this table about?
  2. Headers - What does each column/row represent?
  3. Scope - Which cells do headers apply to?
  4. Cell relationships - Which header goes with which data?

Inaccessible Table (Screen Reader Nightmare):

<!-- No semantic structure -->
<div class="table">
    <div class="row">
        <div class="cell">Name</div>
        <div class="cell">Age</div>
        <div class="cell">City</div>
    </div>
    <div class="row">
        <div class="cell">John</div>
        <div class="cell">30</div>
        <div class="cell">NYC</div>
    </div>
</div>

<!-- Screen reader: "Name, Age, City, John, 30, NYC" -->
<!-- No context! User doesn't know John is a name, 30 is age, etc. -->

Accessible Table (Screen Reader Friendly):

<table>
    <caption>User Information</caption>
    <thead>
        <tr>
            <th scope="col">Name</th>
            <th scope="col">Age</th>
            <th scope="col">City</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>John</td>
            <td>30</td>
            <td>NYC</td>
        </tr>
    </tbody>
</table>

<!-- Screen reader: "Table: User Information, 3 columns, 2 rows" -->
<!-- "Name column, John" "Age column, 30" "City column, NYC" -->
<!-- Clear context and relationships! -->

Impact on Your Business

Accessibility Impact:

  • Data completely unusable - Screen reader users can't understand tables
  • Navigation impossible - Can't move between cells logically
  • WCAG violations - Fails WCAG 2.1 criteria 1.3.1
  • Legal liability - ADA compliance issues
  • Lost customers - Users abandon site when tables are inaccessible

User Experience:

  • Confusion - Even sighted users benefit from clear table structure
  • Poor mobile experience - Responsive tables need proper markup
  • Reduced comprehension - Unclear data relationships
  • Frustration - Users give up trying to understand data

SEO Consequences:

  • Poor content understanding - Search engines struggle with unstructured data
  • Missed rich snippets - Tables can generate rich results
  • Lower rankings - Poorly structured content ranks lower

Real-World Stats:

  • 60% of data tables on websites have accessibility issues
  • Screen reader users spend 3x longer on inaccessible tables
  • 90% abandon task if table is too difficult to navigate

How to Diagnose

Method 1: WAVE Browser Extension

  1. Install WAVE Extension
  2. Visit page with tables
  3. Click WAVE icon
  4. Check for table errors

Look For:

Errors:
⚠ Missing table header
⚠ Table header cell has no text
⚠ Layout table contains th element

Alerts:
⚠ No table caption
⚠ Table missing <thead>

Method 2: Lighthouse Accessibility Audit

  1. Open DevTools (F12)
  2. Go to Lighthouse tab
  3. Check Accessibility
  4. Run audit

Check For:

Accessibility Issues:
⚠ <th> elements do not have a scope attribute
⚠ Tables do not have a <caption>
⚠ <td> elements in a large <table> do not have table headers

Method 3: Manual Code Inspection

Check your HTML for these issues:

<!-- ❌ Using divs instead of table -->
<div class="table">...</div>

<!-- ❌ Missing <th> headers -->
<table>
    <tr>
        <td>Name</td> <!-- Should be <th> -->
        <td>Age</td>
    </tr>
</table>

<!-- ❌ Missing scope attribute -->
<th>Name</th> <!-- Should be <th scope="col"> -->

<!-- ❌ No caption -->
<table>
    <tr>...</tr> <!-- Where's the caption? -->
</table>

<!-- ❌ No <thead> or <tbody> -->
<table>
    <tr><th>Header</th></tr> <!-- Should be in <thead> -->
    <tr><td>Data</td></tr> <!-- Should be in <tbody> -->
</table>

<!-- ❌ Using table for layout -->
<table>
    <tr>
        <td><img src="logo.png"></td>
        <td><nav>Menu</nav></td>
    </tr>
</table>
<!-- Use CSS Grid/Flexbox instead! -->

Method 4: Screen Reader Test

  1. Enable screen reader (NVDA, JAWS, VoiceOver)
  2. Navigate to table
  3. Try to understand data

Test Navigation:

  • Can you tell how many rows/columns?
  • Can you hear column headers when navigating cells?
  • Can you understand which header applies to each cell?
  • Can you navigate by row/column?

VoiceOver (Mac) commands:

  • VO + Table - Table navigation mode
  • VO + C - Read column header
  • VO + R - Read row header

Method 5: Browser DevTools

  1. Open DevTools (F12)
  2. Go to Elements tab
  3. Find table
  4. Check structure

Verify:

  • Table uses <table> element
  • Has <thead> and <tbody>
  • Headers are <th> not <td>
  • Caption present
  • Scope attributes on headers

General Fixes

Fix 1: Basic Table Structure

Convert divs to semantic table:

<!-- BEFORE: Div soup (inaccessible) -->
<div class="table">
    <div class="row">
        <div class="cell">Product</div>
        <div class="cell">Price</div>
    </div>
    <div class="row">
        <div class="cell">Widget</div>
        <div class="cell">$10</div>
    </div>
</div>

<!-- AFTER: Semantic table (accessible) -->
<table>
    <caption>Product Pricing</caption>
    <thead>
        <tr>
            <th scope="col">Product</th>
            <th scope="col">Price</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Widget</td>
            <td>$10</td>
        </tr>
    </tbody>
</table>

Fix 2: Add Proper Headers and Scope

Simple column headers:

<table>
    <caption>Monthly Sales</caption>
    <thead>
        <tr>
            <th scope="col">Month</th>
            <th scope="col">Sales</th>
            <th scope="col">Growth</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>January</td>
            <td>$50,000</td>
            <td>+5%</td>
        </tr>
        <tr>
            <td>February</td>
            <td>$52,500</td>
            <td>+5%</td>
        </tr>
    </tbody>
</table>

Row headers:

<table>
    <caption>Store Locations</caption>
    <thead>
        <tr>
            <th scope="col">Store</th>
            <th scope="col">Address</th>
            <th scope="col">Phone</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">NYC Branch</th>
            <td>123 Main St</td>
            <td>(555) 123-4567</td>
        </tr>
        <tr>
            <th scope="row">LA Branch</th>
            <td>456 Oak Ave</td>
            <td>(555) 987-6543</td>
        </tr>
    </tbody>
</table>

Fix 3: Complex Tables with Headers

Two-dimensional headers:

<table>
    <caption>Quarterly Revenue by Region</caption>
    <thead>
        <tr>
            <th scope="col">Region</th>
            <th scope="col">Q1 2025</th>
            <th scope="col">Q2 2025</th>
            <th scope="col">Q3 2025</th>
            <th scope="col">Q4 2025</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">North America</th>
            <td>$2.5M</td>
            <td>$2.7M</td>
            <td>$3.1M</td>
            <td>$3.4M</td>
        </tr>
        <tr>
            <th scope="row">Europe</th>
            <td>$1.8M</td>
            <td>$1.9M</td>
            <td>$2.2M</td>
            <td>$2.5M</td>
        </tr>
    </tbody>
</table>

Using headers attribute for complex tables:

<table>
    <caption>Product Comparison</caption>
    <thead>
        <tr>
            <th id="product">Product</th>
            <th id="price">Price</th>
            <th id="rating">Rating</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th id="widget" headers="product">Widget</th>
            <td headers="widget price">$10</td>
            <td headers="widget rating">4.5/5</td>
        </tr>
        <tr>
            <th id="gadget" headers="product">Gadget</th>
            <td headers="gadget price">$15</td>
            <td headers="gadget rating">4.8/5</td>
        </tr>
    </tbody>
</table>

Fix 4: Add Table Caption

Descriptive caption:

<!-- BEFORE: No caption -->
<table>
    <tr><th>Name</th><th>Email</th></tr>
</table>

<!-- AFTER: Clear caption -->
<table>
    <caption>Contact Information for Sales Team</caption>
    <thead>
        <tr>
            <th scope="col">Name</th>
            <th scope="col">Email</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>John Smith</td>
            <td>john@example.com</td>
        </tr>
    </tbody>
</table>

<!-- Visually hidden caption (if you want to hide it) -->
<style>
.sr-only {
    position: absolute;
    left: -10000px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}
</style>
<table>
    <caption class="sr-only">Product Pricing Table</caption>
    ...
</table>

Using thead, tbody, tfoot:

<table>
    <caption>Sales Report - Q4 2025</caption>
    <thead>
        <tr>
            <th scope="col">Month</th>
            <th scope="col">Revenue</th>
            <th scope="col">Expenses</th>
            <th scope="col">Profit</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">October</th>
            <td>$100,000</td>
            <td>$60,000</td>
            <td>$40,000</td>
        </tr>
        <tr>
            <th scope="row">November</th>
            <td>$120,000</td>
            <td>$65,000</td>
            <td>$55,000</td>
        </tr>
        <tr>
            <th scope="row">December</th>
            <td>$150,000</td>
            <td>$70,000</td>
            <td>$80,000</td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <th scope="row">Total</th>
            <td>$370,000</td>
            <td>$195,000</td>
            <td>$175,000</td>
        </tr>
    </tfoot>
</table>

Fix 6: Responsive Tables

Mobile-friendly table pattern:

<div class="table-container">
    <table>
        <caption>Product Specifications</caption>
        <thead>
            <tr>
                <th scope="col">Feature</th>
                <th scope="col">Basic</th>
                <th scope="col">Pro</th>
                <th scope="col">Enterprise</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th scope="row">Storage</th>
                <td data-label="Basic">10GB</td>
                <td data-label="Pro">100GB</td>
                <td data-label="Enterprise">Unlimited</td>
            </tr>
            <tr>
                <th scope="row">Users</th>
                <td data-label="Basic">1</td>
                <td data-label="Pro">10</td>
                <td data-label="Enterprise">Unlimited</td>
            </tr>
        </tbody>
    </table>
</div>

<style>
/* Desktop: Normal table */
table {
    width: 100%;
    border-collapse: collapse;
}

/* Mobile: Stack layout */
@media (max-width: 768px) {
    thead {
        display: none; /* Hide headers on mobile */
    }

    tbody, tr, td, th {
        display: block;
        width: 100%;
    }

    td::before {
        content: attr(data-label) ": "; /* Show label before data */
        font-weight: bold;
    }
}
</style>

Fix 7: Sortable Tables

Accessible sortable tables:

<table>
    <caption>Employee Directory</caption>
    <thead>
        <tr>
            <th scope="col">
                <button type="button" aria-label="Sort by name">
                    Name
                    <span aria-hidden="true">↓</span>
                </button>
            </th>
            <th scope="col">
                <button type="button" aria-label="Sort by department">
                    Department
                    <span aria-hidden="true">↓</span>
                </button>
            </th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Alice Johnson</td>
            <td>Engineering</td>
        </tr>
        <tr>
            <td>Bob Smith</td>
            <td>Marketing</td>
        </tr>
    </tbody>
</table>

Fix 8: Avoid Layout Tables

Use CSS instead of tables for layout:

<!-- ❌ WRONG: Table for layout -->
<table>
    <tr>
        <td><img src="logo.png"></td>
        <td>
            <nav>
                <a href="/">Home</a>
                <a href="/about">About</a>
            </nav>
        </td>
    </tr>
</table>

<!-- ✅ CORRECT: CSS Grid for layout -->
<header>
    <img src="logo.png" alt="Company Logo">
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
    </nav>
</header>

<style>
header {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 20px;
    align-items: center;
}
</style>

Platform-Specific Guides

Detailed implementation instructions for your specific platform:

Platform Troubleshooting Guide
Shopify Shopify Table Structure Guide
WordPress WordPress Table Structure Guide
Wix Wix Table Structure Guide
Squarespace Squarespace Table Structure Guide
Webflow Webflow Table Structure Guide

Verification

After implementing proper table structure:

Test 1: WAVE Extension

  1. Run WAVE scan
  2. Check for table errors cleared
  3. Verify green checkmarks for tables

Test 2: Lighthouse

  1. Run accessibility audit
  2. Table-related issues should be resolved
  3. Score should improve

Test 3: Screen Reader

  1. Navigate to table
  2. Screen reader should announce:
    • Table caption
    • Number of rows/columns
    • Column headers when navigating cells
  3. Test navigation by row/column

Test 4: Browser DevTools

// Check table structure
const table = document.querySelector('table');
console.log('Has caption:', !!table.querySelector('caption'));
console.log('Has thead:', !!table.querySelector('thead'));
console.log('Has tbody:', !!table.querySelector('tbody'));
console.log('Header cells:', table.querySelectorAll('th').length);
console.log('Cells with scope:', table.querySelectorAll('th[scope]').length);

Common Mistakes

  1. Using tables for layout - Use CSS Grid/Flexbox
  2. Missing caption - Every table needs context
  3. td instead of th - Headers must be <th> elements
  4. Missing scope attribute - Add scope="col" or scope="row"
  5. No thead/tbody - Group headers and data
  6. Empty headers - All headers need text (or aria-label)
  7. Merged cells without colspan/rowspan - Use proper attributes
  8. Missing role="table" - Only needed when not using <table> (rare)

Further Reading

// SYS.FOOTER