When to Use HTML Tables

Use <table> for tabular data — financial reports, schedules, comparison charts. Never use tables for page layout; CSS Grid and Flexbox handle layout better and don’t confuse assistive technology.

Complete Table Anatomy

  <table>
    <caption>2024 Quarterly Revenue (USD)</caption>
    <colgroup>
        <col span="1" style="background: #f8f9fa">
        <col span="3">
    </colgroup>
    <thead>
        <tr>
            <th scope="col" id="quarter">Quarter</th>
            <th scope="col" id="product-a">Product A</th>
            <th scope="col" id="product-b">Product B</th>
            <th scope="col" id="total">Total</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row" headers="quarter">Q1</th>
            <td headers="quarter product-a">$10,000</td>
            <td headers="quarter product-b">$8,500</td>
            <td headers="quarter total">$18,500</td>
        </tr>
        <tr>
            <th scope="row" headers="quarter">Q2</th>
            <td headers="quarter product-a">$12,400</td>
            <td headers="quarter product-b">$9,200</td>
            <td headers="quarter total">$21,600</td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <th scope="row" headers="quarter">Annual</th>
            <td headers="product-a">$22,400</td>
            <td headers="product-b">$17,700</td>
            <td headers="total"><strong>$40,100</strong></td>
        </tr>
    </tfoot>
</table>
  

Accessibility Requirements

Screen readers announce table dimensions and header associations. Follow these rules:

  1. <caption> — first child of <table>; describes the table purpose
  2. <th scope="col|row"> — identifies header direction
  3. headers attribute — on <td> when headers aren’t simple (complex tables)
  4. Don’t use scope on <td> — only on <th>

For very complex tables (nested headers), use explicit id on headers and headers on cells.

Column and Row Spanning

  <thead>
    <tr>
        <th rowspan="2" scope="col">Region</th>
        <th colspan="2" scope="colgroup">Sales</th>
    </tr>
    <tr>
        <th scope="col">Online</th>
        <th scope="col">Retail</th>
    </tr>
</thead>
  

colspan spans columns; rowspan spans rows. Validate that every data cell has a logical header.

Sortable Data Tables (HTML + JS Pattern)

  <table id="users">
    <thead>
        <tr>
            <th scope="col"><button type="button" data-sort="name">Name ↕</button></th>
            <th scope="col"><button type="button" data-sort="email">Email ↕</button></th>
        </tr>
    </thead>
    <tbody>...</tbody>
</table>
  

Use <button> inside <th> for sort controls — buttons are keyboard accessible. Announce sort state with aria-sort="ascending|descending|none".

Responsive Table Patterns

Horizontal Scroll Wrapper

  <div style="overflow-x: auto;">
    <table>...</table>
</div>
  

Simplest approach — preserves table semantics.

Stacked Cards (Mobile)

  @media (max-width: 600px) {
    table, thead, tbody, tr, th, td { display: block; }
    thead { position: absolute; left: -9999px; }
    td::before {
        content: attr(data-label);
        font-weight: bold;
        display: block;
    }
}
  
  <td data-label="Email">[email protected]</td>
  

Hide Non-Essential Columns

  @media (max-width: 768px) {
    .col-secondary { display: none; }
}
  

Styling Tables

  table {
    width: 100%;
    border-collapse: collapse;
}
th, td {
    padding: 0.75rem 1rem;
    text-align: left;
    border-bottom: 1px solid #dee2e6;
}
tbody tr:hover { background: #f8f9fa; }
caption { caption-side: top; font-weight: bold; margin-bottom: 0.5rem; }
  

Use border-collapse: collapse for clean borders. Zebra striping: tbody tr:nth-child(even) { background: #f8f9fa; }.

<colgroup> and <col>

Style entire columns without classes on every cell:

  <colgroup>
    <col class="col-label">
    <col span="3" class="col-data">
</colgroup>
  
  .col-label { background: #e9ecef; width: 30%; }
  

Common Mistakes

Mistake Fix
Layout tables Use CSS Grid
Missing <caption> Add descriptive caption
Header cells as <td> Use <th scope="...">
Empty cells for spacing CSS padding on <td>

Troubleshooting

Screen reader reads wrong column header

  • Add scope="col" or scope="row"; use headers for complex tables

Table overflows on mobile

  • Wrap in scroll container or implement stacked pattern

Sort breaks after DOM update

  • Re-apply aria-sort; maintain focus on sort button

Tables are powerful for structured data — build them accessibly first, then enhance with CSS and JavaScript.