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:
- Caption - What is this table about?
- Headers - What does each column/row represent?
- Scope - Which cells do headers apply to?
- 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
- Install WAVE Extension
- Visit page with tables
- Click WAVE icon
- 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
- Open DevTools (
F12) - Go to Lighthouse tab
- Check Accessibility
- 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
- Enable screen reader (NVDA, JAWS, VoiceOver)
- Navigate to table
- 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 modeVO + C- Read column headerVO + R- Read row header
Method 5: Browser DevTools
- Open DevTools (
F12) - Go to Elements tab
- Find table
- 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>
Fix 5: Group Related Data
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:
Verification
After implementing proper table structure:
Test 1: WAVE Extension
- Run WAVE scan
- Check for table errors cleared
- Verify green checkmarks for tables
Test 2: Lighthouse
- Run accessibility audit
- Table-related issues should be resolved
- Score should improve
Test 3: Screen Reader
- Navigate to table
- Screen reader should announce:
- Table caption
- Number of rows/columns
- Column headers when navigating cells
- 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
- Using tables for layout - Use CSS Grid/Flexbox
- Missing caption - Every table needs context
- td instead of th - Headers must be
<th>elements - Missing scope attribute - Add
scope="col"orscope="row" - No thead/tbody - Group headers and data
- Empty headers - All headers need text (or aria-label)
- Merged cells without colspan/rowspan - Use proper attributes
- Missing role="table" - Only needed when not using
<table>(rare)