· Tutorial

HTML Tables: When and How to Use Them

Master data tables and learn when tables are the right choice.

HTML tables are designed for one thing: displaying tabular data—information that naturally fits in rows and columns. Despite their reputation from the bad old days of table-based layouts, tables are essential for presenting structured data like schedules, pricing charts, or financial reports. In this guide, you’ll learn when to use tables, how to build them correctly, and how to make them accessible and responsive.

Remember: Tables are for data, not for page layout. That’s what CSS is for!

When to Use Tables

✅ Good Uses for Tables

Tables are perfect for data that has a clear relationship between rows and columns:

  • Financial data - Budgets, invoices, balance sheets
  • Schedules - Calendars, timetables, class schedules
  • Comparisons - Product features, pricing plans
  • Statistics - Sports scores, survey results, analytics
  • Lists with multiple attributes - Employee directories, product catalogs

❌ Don’t Use Tables For

  • Page layout - Use CSS Grid or Flexbox instead
  • Navigation menus - Use lists (<ul>, <li>)
  • Image galleries - Use CSS Grid
  • Form layouts - Use proper form elements and CSS
  • Any non-tabular content - If it doesn’t need rows and columns, don’t use a table

Basic Table Structure

Every table needs these core elements:

<table>
  <thead>
    <tr>
      <th>Product</th>
      <th>Price</th>
      <th>Stock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Widget</td>
      <td>$19.99</td>
      <td>47</td>
    </tr>
    <tr>
      <td>Gadget</td>
      <td>$29.99</td>
      <td>23</td>
    </tr>
  </tbody>
</table>

Key elements:

  • <table> - The container
  • <thead> - Table header section
  • <tbody> - Table body section
  • <tr> - Table row
  • <th> - Table header cell (bold by default)
  • <td> - Table data cell

Adding a Caption

Always add a caption to describe what the table shows:

<table>
  <caption>Q4 2023 Sales by Region</caption>
  <thead>
    <tr>
      <th>Region</th>
      <th>Revenue</th>
      <th>Growth</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>North America</td>
      <td>$2.4M</td>
      <td>+12%</td>
    </tr>
    <tr>
      <td>Europe</td>
      <td>$1.8M</td>
      <td>+8%</td>
    </tr>
  </tbody>
</table>

The <caption> element helps screen reader users understand the table before diving into the data.

Table Headers with Scope

Use the scope attribute to explicitly define what each header describes:

<table>
  <thead>
    <tr>
      <th scope="col">Product</th>
      <th scope="col">Q1</th>
      <th scope="col">Q2</th>
      <th scope="col">Q3</th>
      <th scope="col">Q4</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Revenue</th>
      <td>$1.2M</td>
      <td>$1.5M</td>
      <td>$1.8M</td>
      <td>$2.1M</td>
    </tr>
    <tr>
      <th scope="row">Expenses</th>
      <td>$800K</td>
      <td>$850K</td>
      <td>$920K</td>
      <td>$980K</td>
    </tr>
  </tbody>
</table>
  • scope="col" - Header describes a column
  • scope="row" - Header describes a row

This helps screen readers announce “Revenue, Q1: $1.2M” instead of just reading numbers.

Spanning Rows and Columns

Use colspan and rowspan for cells that span multiple columns or rows:

<table>
  <caption>Product Comparison</caption>
  <thead>
    <tr>
      <th>Feature</th>
      <th>Basic</th>
      <th>Pro</th>
      <th>Enterprise</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Price</th>
      <td>$9/mo</td>
      <td>$29/mo</td>
      <td>Custom</td>
    </tr>
    <tr>
      <th scope="row">Users</th>
      <td>1</td>
      <td>10</td>
      <td>Unlimited</td>
    </tr>
    <tr>
      <th scope="row">Support</th>
      <td colspan="2">Email only</td>
      <td>24/7 Phone + Email</td>
    </tr>
  </tbody>
</table>

The colspan="2" makes that cell span two columns.

Use <tfoot> for summary rows:

<table>
  <caption>Monthly Expenses</caption>
  <thead>
    <tr>
      <th scope="col">Category</th>
      <th scope="col">Amount</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Rent</th>
      <td>$1,200</td>
    </tr>
    <tr>
      <th scope="row">Utilities</th>
      <td>$150</td>
    </tr>
    <tr>
      <th scope="row">Internet</th>
      <td>$60</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>$1,410</td>
    </tr>
  </tfoot>
</table>

Styling Tables with CSS

Basic table styling for readability:

<style>
  table {
    width: 100%;
    border-collapse: collapse;
    font-family: system-ui, sans-serif;
  }
  
  th, td {
    padding: 0.75rem;
    text-align: left;
    border-bottom: 1px solid #ddd;
  }
  
  th {
    background: #f8f9fa;
    font-weight: 600;
    color: #333;
  }
  
  tbody tr:hover {
    background: #f8f9fa;
  }
  
  caption {
    font-weight: bold;
    font-size: 1.25rem;
    margin-bottom: 0.5rem;
    text-align: left;
  }
  
  tfoot {
    font-weight: bold;
    background: #e9ecef;
  }
</style>

<table>
  <caption>Employee Directory</caption>
  <thead>
    <tr>
      <th>Name</th>
      <th>Department</th>
      <th>Email</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Jane Smith</td>
      <td>Engineering</td>
      <td>[email protected]</td>
    </tr>
    <tr>
      <td>John Doe</td>
      <td>Marketing</td>
      <td>[email protected]</td>
    </tr>
  </tbody>
</table>

Responsive Tables

Tables can be challenging on small screens. Here are three approaches:

1. Horizontal Scroll

.table-container {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

table {
  min-width: 600px;
}
<div class="table-container">
  <table>
    <!-- Table content -->
  </table>
</div>

2. Stackable Rows

@media (max-width: 768px) {
  table, thead, tbody, th, td, tr {
    display: block;
  }
  
  thead tr {
    display: none;
  }
  
  td {
    text-align: right;
    padding-left: 50%;
    position: relative;
  }
  
  td:before {
    content: attr(data-label);
    position: absolute;
    left: 0;
    padding-left: 15px;
    font-weight: bold;
    text-align: left;
  }
}
<table>
  <thead>
    <tr>
      <th>Product</th>
      <th>Price</th>
      <th>Stock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td data-label="Product">Widget</td>
      <td data-label="Price">$19.99</td>
      <td data-label="Stock">47</td>
    </tr>
  </tbody>
</table>

3. Side-by-Side Cards

For small tables, convert to cards on mobile:

@media (max-width: 768px) {
  table, thead, tbody, tr {
    display: block;
  }
  
  thead {
    display: none;
  }
  
  tr {
    margin-bottom: 1rem;
    border: 1px solid #ddd;
    border-radius: 8px;
    padding: 1rem;
  }
  
  td {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem 0;
  }
  
  td:before {
    content: attr(data-label);
    font-weight: bold;
  }
}

Accessible Tables Checklist

Make your tables accessible to everyone:

  • ✅ Use <caption> to describe the table
  • ✅ Use <th> for headers, not <td>
  • ✅ Add scope attribute to headers
  • ✅ Use <thead>, <tbody>, <tfoot> to group rows
  • ✅ Don’t merge cells unless necessary
  • ✅ Provide alternative views for complex tables
  • ✅ Test with a screen reader
  • ✅ Ensure keyboard navigation works
  • ✅ Use sufficient color contrast
  • ✅ Don’t rely on color alone to convey information

Complete Example

Here’s a fully-featured, accessible table:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Accessible Table Example</title>
  <style>
    body {
      font-family: system-ui, sans-serif;
      max-width: 900px;
      margin: 2rem auto;
      padding: 0 1rem;
    }
    
    table {
      width: 100%;
      border-collapse: collapse;
      margin: 2rem 0;
    }
    
    caption {
      font-size: 1.5rem;
      font-weight: bold;
      text-align: left;
      margin-bottom: 1rem;
      color: #333;
    }
    
    th, td {
      padding: 1rem;
      text-align: left;
      border: 1px solid #ddd;
    }
    
    thead th {
      background: #0066cc;
      color: white;
      font-weight: 600;
    }
    
    tbody tr:nth-child(even) {
      background: #f8f9fa;
    }
    
    tbody tr:hover {
      background: #e9ecef;
    }
    
    tfoot {
      background: #f8f9fa;
      font-weight: bold;
    }
    
    .price {
      text-align: right;
      font-family: 'Courier New', monospace;
    }
  </style>
</head>
<body>
  <table>
    <caption>Pricing Plans Comparison</caption>
    <thead>
      <tr>
        <th scope="col">Feature</th>
        <th scope="col">Starter</th>
        <th scope="col">Professional</th>
        <th scope="col">Enterprise</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Monthly Price</th>
        <td class="price">$9</td>
        <td class="price">$29</td>
        <td class="price">$99</td>
      </tr>
      <tr>
        <th scope="row">Users</th>
        <td>1</td>
        <td>5</td>
        <td>Unlimited</td>
      </tr>
      <tr>
        <th scope="row">Storage</th>
        <td>10 GB</td>
        <td>100 GB</td>
        <td>1 TB</td>
      </tr>
      <tr>
        <th scope="row">Support</th>
        <td>Email</td>
        <td>Priority Email</td>
        <td>24/7 Phone + Email</td>
      </tr>
      <tr>
        <th scope="row">API Access</th>
        <td>❌</td>
        <td>✅</td>
        <td>✅</td>
      </tr>
      <tr>
        <th scope="row">Custom Domain</th>
        <td>❌</td>
        <td>✅</td>
        <td>✅</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <td>Annual Savings</td>
        <td class="price">$0</td>
        <td class="price">$49</td>
        <td class="price">$189</td>
      </tr>
    </tfoot>
  </table>
</body>
</html>

Common Table Mistakes

Using tables for layout - Use CSS Grid/Flexbox ❌ Forgetting <thead> and <tbody> - Improves structure ❌ No caption - Screen readers need context ❌ Using <td> for headers - Always use <th>No scope attribute - Screen readers struggle ❌ Overly complex tables - Simplify when possible ❌ Not making tables responsive - Test on mobile ❌ Using color only to convey meaning - Bad accessibility

When to Use Lists Instead

If your data doesn’t need both rows AND columns, use a list:

<!-- ❌ Overkill table -->
<table>
  <tr><td>Item 1</td></tr>
  <tr><td>Item 2</td></tr>
  <tr><td>Item 3</td></tr>
</table>

<!-- ✅ Simple list -->
<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

Keep Learning

Try building tables in the htmlEditor.net playground right now!

Remember: Tables are powerful tools for displaying structured data. Use them wisely, make them accessible, and they’ll serve your users well.

← Back to all blog posts

    Share: