Tables and Tabular Data

1. Table Structure (table, thead, tbody, tfoot)

Element Purpose Parent Required
<table> Root table container Any block element Yes
<caption> Table title/description <table> (first child) Recommended
<thead> Table header section <table> Optional
<tbody> Table body section (main data) <table> Optional (implicit)
<tfoot> Table footer section (summary) <table> Optional
<tr> Table row <thead>, <tbody>, <tfoot>, <table> Yes
<th> Header cell <tr> In header rows
<td> Data cell <tr> In data rows
<colgroup> Group of columns for styling <table> (after caption, before thead) Optional
<col> Single column definition <colgroup> Optional

Table Attributes

Attribute Element Purpose
border <table> Border width (deprecated, use CSS)
cellpadding <table> Cell padding (deprecated, use CSS)
cellspacing <table> Cell spacing (deprecated, use CSS)
width <table>, <col> Table/column width (use CSS)
Section Order:
  1. <caption> (optional, first)
  2. <colgroup> (optional)
  3. <thead> (optional)
  4. <tbody> (optional, can have multiple)
  5. <tfoot> (optional, displays at bottom)

Note: <tfoot> can be placed before <tbody> in HTML but will render at the bottom.

Example: Proper table structure

<!-- Complete table structure -->
<table>
  <caption>Quarterly Sales Report 2024</caption>
  
  <colgroup>
    <col style="background-color: #f0f0f0;">
    <col span="3" style="background-color: #fff;">
  </colgroup>
  
  <thead>
    <tr>
      <th>Quarter</th>
      <th>Revenue</th>
      <th>Expenses</th>
      <th>Profit</th>
    </tr>
  </thead>
  
  <tbody>
    <tr>
      <th>Q1</th>
      <td>$100,000</td>
      <td>$60,000</td>
      <td>$40,000</td>
    </tr>
    <tr>
      <th>Q2</th>
      <td>$120,000</td>
      <td>$65,000</td>
      <td>$55,000</td>
    </tr>
    <tr>
      <th>Q3</th>
      <td>$110,000</td>
      <td>$62,000</td>
      <td>$48,000</td>
    </tr>
    <tr>
      <th>Q4</th>
      <td>$150,000</td>
      <td>$70,000</td>
      <td>$80,000</td>
    </tr>
  </tbody>
  
  <tfoot>
    <tr>
      <th>Total</th>
      <td>$480,000</td>
      <td>$257,000</td>
      <td>$223,000</td>
    </tr>
  </tfoot>
</table>

<!-- Simple table (minimal structure) -->
<table>
  <caption>Employee Contact List</caption>
  <tr>
    <th>Name</th>
    <th>Email</th>
    <th>Phone</th>
  </tr>
  <tr>
    <td>John Doe</td>
    <td>john@example.com</td>
    <td>555-1234</td>
  </tr>
  <tr>
    <td>Jane Smith</td>
    <td>jane@example.com</td>
    <td>555-5678</td>
  </tr>
</table>

<!-- Multiple tbody sections -->
<table>
  <caption>Departments</caption>
  <thead>
    <tr>
      <th>Employee</th>
      <th>Role</th>
    </tr>
  </thead>
  
  <tbody>
    <tr>
      <th colspan="2">Engineering</th>
    </tr>
    <tr>
      <td>Alice</td>
      <td>Developer</td>
    </tr>
    <tr>
      <td>Bob</td>
      <td>Designer</td>
    </tr>
  </tbody>
  
  <tbody>
    <tr>
      <th colspan="2">Sales</th>
    </tr>
    <tr>
      <td>Charlie</td>
      <td>Manager</td>
    </tr>
    <tr>
      <td>Diana</td>
      <td>Representative</td>
    </tr>
  </tbody>
</table>
Note: Use <thead>, <tbody>, and <tfoot> for accessibility and to enable features like fixed headers on scroll. Browser implicitly creates <tbody> if omitted.

2. Table Rows and Cells (tr, td, th)

Element Attributes Purpose Use Case
<tr> class, id, style Define table row Container for cells
<th> scope, colspan, rowspan, headers Header cell (bold, centered) Column/row headers
<td> colspan, rowspan, headers Data cell Table data values

TH Scope Attribute

Value Meaning
col Header for column
row Header for row
colgroup Header for column group
rowgroup Header for row group

Cell Alignment (CSS)

Property Values
text-align left, center, right
vertical-align top, middle, bottom

Example: Table rows and cells with scope

<!-- Column headers with scope -->
<table>
  <thead>
    <tr>
      <th scope="col">Product</th>
      <th scope="col">Price</th>
      <th scope="col">Quantity</th>
      <th scope="col">Total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Widget</td>
      <td>$10.00</td>
      <td>5</td>
      <td>$50.00</td>
    </tr>
    <tr>
      <td>Gadget</td>
      <td>$25.00</td>
      <td>2</td>
      <td>$50.00</td>
    </tr>
  </tbody>
</table>

<!-- Row headers with scope -->
<table>
  <caption>Monthly Expenses</caption>
  <thead>
    <tr>
      <th></th>
      <th scope="col">January</th>
      <th scope="col">February</th>
      <th scope="col">March</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Rent</th>
      <td>$1,500</td>
      <td>$1,500</td>
      <td>$1,500</td>
    </tr>
    <tr>
      <th scope="row">Utilities</th>
      <td>$200</td>
      <td>$220</td>
      <td>$180</td>
    </tr>
    <tr>
      <th scope="row">Food</th>
      <td>$600</td>
      <td>$650</td>
      <td>$580</td>
    </tr>
  </tbody>
</table>

<!-- Complex table with headers attribute -->
<table>
  <tr>
    <th id="name">Name</th>
    <th id="math">Math</th>
    <th id="science">Science</th>
  </tr>
  <tr>
    <td headers="name">Alice</td>
    <td headers="math">95</td>
    <td headers="science">88</td>
  </tr>
  <tr>
    <td headers="name">Bob</td>
    <td headers="math">87</td>
    <td headers="science">92</td>
  </tr>
</table>

<!-- Styled cells -->
<style>
  table {
    border-collapse: collapse;
    width: 100%;
  }
  
  th, td {
    border: 1px solid #ddd;
    padding: 12px;
    text-align: left;
  }
  
  th {
    background-color: #4caf50;
    color: white;
    text-align: center;
  }
  
  tr:nth-child(even) {
    background-color: #f2f2f2;
  }
  
  tr:hover {
    background-color: #ddd;
  }
</style>
Warning: Always use scope attribute on <th> elements for accessibility. Screen readers use this to associate headers with data cells. Use scope="col" for column headers and scope="row" for row headers.

3. Cell Spanning (colspan, rowspan)

Attribute Applies To Purpose Default
colspan <td>, <th> Span across multiple columns 1
rowspan <td>, <th> Span across multiple rows 1

Colspan Example (visual)

Spanning 3 Columns
Col 1 Col 2 Col 3

Rowspan Example (visual)

Spanning 2 Rows Row 1
Row 2

Example: Colspan and rowspan usage

<!-- Colspan: Header spanning columns -->
<table>
  <thead>
    <tr>
      <th colspan="3">Student Performance</th>
    </tr>
    <tr>
      <th>Name</th>
      <th>Math</th>
      <th>Science</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alice</td>
      <td>95</td>
      <td>88</td>
    </tr>
  </tbody>
</table>

<!-- Rowspan: Merged rows -->
<table>
  <tr>
    <th rowspan="2">Name</th>
    <th colspan="2">Scores</th>
  </tr>
  <tr>
    <!-- Name cell continues from above -->
    <th>Test 1</th>
    <th>Test 2</th>
  </tr>
  <tr>
    <td>John</td>
    <td>85</td>
    <td>90</td>
  </tr>
</table>

<!-- Combined colspan and rowspan -->
<table>
  <caption>Complex Schedule</caption>
  <thead>
    <tr>
      <th>Time</th>
      <th>Monday</th>
      <th>Tuesday</th>
      <th>Wednesday</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>9:00 AM</th>
      <td rowspan="2">Team Meeting</td>
      <td>Project A</td>
      <td>Project B</td>
    </tr>
    <tr>
      <th>10:00 AM</th>
      <!-- Team Meeting continues -->
      <td>Code Review</td>
      <td>Planning</td>
    </tr>
    <tr>
      <th>11:00 AM</th>
      <td colspan="3">Lunch Break</td>
    </tr>
  </tbody>
</table>

<!-- Pricing table with spanning -->
<table>
  <thead>
    <tr>
      <th rowspan="2">Feature</th>
      <th colspan="3">Plans</th>
    </tr>
    <tr>
      <th>Basic</th>
      <th>Pro</th>
      <th>Enterprise</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Storage</td>
      <td>10 GB</td>
      <td>100 GB</td>
      <td>Unlimited</td>
    </tr>
    <tr>
      <td>Users</td>
      <td>1</td>
      <td>5</td>
      <td>Unlimited</td>
    </tr>
    <tr>
      <td>Support</td>
      <td>Email</td>
      <td colspan="2">24/7 Phone & Email</td>
    </tr>
  </tbody>
</table>
Note: When using rowspan, omit cells in subsequent rows that are spanned. When using colspan, ensure the total columns in each row match. Complex spanning can make tables difficult to maintain and less accessible.

4. Table Captions and Summaries

Element/Attribute Purpose Placement Accessibility
<caption> Table title/description First child of <table> Announced by screen readers
summary (deprecated) Table description for screen readers <table summary="..."> Use <caption> or aria-describedby instead
aria-label Accessible name for table <table aria-label="..."> Alternative to caption
aria-describedby Reference to description element <table aria-describedby="id"> Link to external description

Caption Styling (CSS)

Property Values
caption-side top (default), bottom
text-align left, center, right
font-weight normal, bold
Best Practices:
  • Use <caption> for all data tables
  • Keep captions concise and descriptive
  • Position caption at top for visibility
  • Use aria-describedby for longer descriptions
  • Avoid summary attribute (deprecated)

Example: Table captions and descriptions

<!-- Basic caption -->
<table>
  <caption>Monthly Sales Report - Q4 2024</caption>
  <thead>
    <tr>
      <th>Month</th>
      <th>Revenue</th>
      <th>Growth</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>October</td>
      <td>$150,000</td>
      <td>+15%</td>
    </tr>
  </tbody>
</table>

<!-- Styled caption -->
<style>
  caption {
    caption-side: top;
    font-size: 1.2em;
    font-weight: bold;
    margin-bottom: 10px;
    text-align: left;
    color: #333;
  }
</style>

<table>
  <caption>Employee Directory</caption>
  <!-- table content -->
</table>

<!-- Caption at bottom -->
<style>
  .bottom-caption caption {
    caption-side: bottom;
    font-style: italic;
    margin-top: 10px;
  }
</style>

<table class="bottom-caption">
  <caption>Data as of December 2024</caption>
  <!-- table content -->
</table>

<!-- Using aria-describedby for detailed description -->
<p id="table-desc">
  This table shows quarterly performance metrics including revenue, 
  expenses, and profit margins for each department. Data is sorted 
  by revenue in descending order.
</p>

<table aria-describedby="table-desc">
  <caption>2024 Quarterly Performance by Department</caption>
  <thead>
    <tr>
      <th>Department</th>
      <th>Revenue</th>
      <th>Expenses</th>
      <th>Profit Margin</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sales</td>
      <td>$500,000</td>
      <td>$200,000</td>
      <td>60%</td>
    </tr>
  </tbody>
</table>

<!-- Using aria-label (when caption is not desired visually) -->
<table aria-label="Customer feedback ratings by product">
  <!-- No visible caption, but screen readers announce the label -->
  <thead>
    <tr>
      <th>Product</th>
      <th>Rating</th>
      <th>Reviews</th>
    </tr>
  </thead>
</table>

<!-- Caption with formatting -->
<table>
  <caption>
    <strong>Financial Summary</strong><br>
    <small>All amounts in USD</small>
  </caption>
  <!-- table content -->
</table>
Warning: The summary attribute is deprecated in HTML5. Use <caption> for short descriptions or aria-describedby to reference a detailed description elsewhere in the page.

5. Sortable Tables and Interactive Features

Feature Implementation Use Case Accessibility
Sortable Columns JavaScript + click events on <th> Sort data by column Add aria-sort attribute
Filterable Rows JavaScript + input field Search/filter table data Announce filtered count
Expandable Rows JavaScript + detail rows Show/hide additional details Use aria-expanded
Selectable Rows Checkboxes in first column Multi-select for bulk actions Proper label association
Pagination JavaScript + page controls Handle large datasets Announce current page

ARIA Attributes for Tables

Attribute Purpose
aria-sort ascending, descending, none
aria-expanded true/false for expandable rows
aria-selected true/false for selected rows
aria-rowcount Total rows (for pagination)
aria-rowindex Current row index
JavaScript Table Libraries:
  • DataTables: Feature-rich table plugin
  • Tabulator: Interactive tables with filtering
  • AG Grid: Enterprise data grid
  • TanStack Table: Headless table library
  • SortableJS: Drag-and-drop sorting

Example: Sortable table with JavaScript

<!-- Sortable table -->
<table id="sortableTable">
  <caption>Employee List (Click headers to sort)</caption>
  <thead>
    <tr>
      <th onclick="sortTable(0)" style="cursor: pointer;" aria-sort="none">
        Name ↕
      </th>
      <th onclick="sortTable(1)" style="cursor: pointer;" aria-sort="none">
        Department ↕
      </th>
      <th onclick="sortTable(2)" style="cursor: pointer;" aria-sort="none">
        Salary ↕
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Alice</td>
      <td>Engineering</td>
      <td>90000</td>
    </tr>
    <tr>
      <td>Bob</td>
      <td>Sales</td>
      <td>75000</td>
    </tr>
    <tr>
      <td>Charlie</td>
      <td>Marketing</td>
      <td>80000</td>
    </tr>
  </tbody>
</table>

<script>
  function sortTable(columnIndex) {
    const table = document.getElementById('sortableTable');
    const tbody = table.querySelector('tbody');
    const rows = Array.from(tbody.querySelectorAll('tr'));
    const header = table.querySelectorAll('th')[columnIndex];
    
    // Determine sort direction
    const currentSort = header.getAttribute('aria-sort');
    const ascending = currentSort !== 'ascending';
    
    // Sort rows
    rows.sort((a, b) => {
      const aValue = a.cells[columnIndex].textContent;
      const bValue = b.cells[columnIndex].textContent;
      
      // Numeric comparison for salary column
      if (columnIndex === 2) {
        return ascending 
          ? Number(aValue) - Number(bValue)
          : Number(bValue) - Number(aValue);
      }
      
      // String comparison
      return ascending
        ? aValue.localeCompare(bValue)
        : bValue.localeCompare(aValue);
    });
    
    // Re-append sorted rows
    rows.forEach(row => tbody.appendChild(row));
    
    // Update ARIA attributes
    table.querySelectorAll('th').forEach(th => {
      th.setAttribute('aria-sort', 'none');
    });
    header.setAttribute('aria-sort', ascending ? 'ascending' : 'descending');
  }
</script>

<!-- Filterable table -->
<input type="text" id="searchInput" placeholder="Search table..." 
       onkeyup="filterTable()">
<table id="filterableTable">
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>City</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>John Doe</td>
      <td>john@example.com</td>
      <td>New York</td>
    </tr>
    <tr>
      <td>Jane Smith</td>
      <td>jane@example.com</td>
      <td>Los Angeles</td>
    </tr>
  </tbody>
</table>

<script>
  function filterTable() {
    const input = document.getElementById('searchInput');
    const filter = input.value.toLowerCase();
    const table = document.getElementById('filterableTable');
    const rows = table.querySelectorAll('tbody tr');
    
    rows.forEach(row => {
      const text = row.textContent.toLowerCase();
      row.style.display = text.includes(filter) ? '' : 'none';
    });
  }
</script>

<!-- Selectable rows with checkboxes -->
<table>
  <thead>
    <tr>
      <th>
        <input type="checkbox" id="selectAll" onclick="toggleAll(this)">
      </th>
      <th>Task</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><input type="checkbox" class="row-select"></td>
      <td>Design mockup</td>
      <td>Complete</td>
    </tr>
    <tr>
      <td><input type="checkbox" class="row-select"></td>
      <td>Write tests</td>
      <td>In Progress</td>
    </tr>
  </tbody>
</table>

<script>
  function toggleAll(source) {
    const checkboxes = document.querySelectorAll('.row-select');
    checkboxes.forEach(cb => cb.checked = source.checked);
  }
</script>
Note: When implementing sortable tables, update aria-sort attributes to indicate sort state. Use role="button" on sortable headers and ensure keyboard accessibility (Enter/Space to activate).

6. Responsive Table Techniques

Technique CSS/HTML Pros Cons
Horizontal Scroll overflow-x: auto Simple, preserves layout Hidden content, poor UX
Stacked Cells Display: block on mobile All data visible Loses table semantics
Hidden Columns Hide less important columns Keeps key data visible Data loss on mobile
Flip Layout Transpose rows/columns Better for narrow screens Complex CSS
Cards/List View Convert to card layout Mobile-friendly Loses table semantics

Example: Responsive table implementations

<!-- Method 1: Horizontal scroll wrapper -->
<div style="overflow-x: auto;">
  <table style="min-width: 600px;">
    <thead>
      <tr>
        <th>Name</th>
        <th>Email</th>
        <th>Phone</th>
        <th>Department</th>
        <th>Location</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>John Doe</td>
        <td>john@example.com</td>
        <td>555-1234</td>
        <td>Engineering</td>
        <td>New York</td>
      </tr>
    </tbody>
  </table>
</div>

<!-- Method 2: Stacked cells on mobile -->
<style>
  @media (max-width: 768px) {
    .responsive-table thead {
      display: none;
    }
    
    .responsive-table tr {
      display: block;
      margin-bottom: 20px;
      border: 1px solid #ddd;
    }
    
    .responsive-table td {
      display: block;
      text-align: right;
      padding: 10px;
      border-bottom: 1px solid #eee;
    }
    
    .responsive-table td::before {
      content: attr(data-label);
      float: left;
      font-weight: bold;
    }
    
    .responsive-table td:last-child {
      border-bottom: none;
    }
  }
</style>

<table class="responsive-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">50</td>
    </tr>
    <tr>
      <td data-label="Product">Gadget</td>
      <td data-label="Price">$29.99</td>
      <td data-label="Stock">25</td>
    </tr>
  </tbody>
</table>

<!-- Method 3: Hide columns on mobile -->
<style>
  @media (max-width: 768px) {
    .hide-mobile {
      display: none;
    }
  }
</style>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th class="hide-mobile">Phone</th>
      <th class="hide-mobile">Address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>John</td>
      <td>john@example.com</td>
      <td class="hide-mobile">555-1234</td>
      <td class="hide-mobile">123 Main St</td>
    </tr>
  </tbody>
</table>

<!-- Method 4: Card layout on mobile -->
<style>
  .card-table {
    width: 100%;
    border-collapse: collapse;
  }
  
  @media (max-width: 768px) {
    .card-table,
    .card-table thead,
    .card-table tbody,
    .card-table tr,
    .card-table th,
    .card-table td {
      display: block;
    }
    
    .card-table thead {
      display: none;
    }
    
    .card-table tr {
      margin-bottom: 15px;
      background: #f9f9f9;
      border-radius: 8px;
      padding: 15px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    
    .card-table td {
      padding: 5px 0;
      border: none;
    }
    
    .card-table td::before {
      content: attr(data-label) ": ";
      font-weight: bold;
      display: inline-block;
      min-width: 100px;
    }
  }
</style>

<table class="card-table">
  <thead>
    <tr>
      <th>Name</th>
      <th>Role</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td data-label="Name">Alice Johnson</td>
      <td data-label="Role">Developer</td>
      <td data-label="Status">Active</td>
    </tr>
  </tbody>
</table>

<!-- Method 5: Flip table (transpose) -->
<style>
  @media (max-width: 768px) {
    .flip-table {
      display: block;
    }
    
    .flip-table thead {
      float: left;
    }
    
    .flip-table tbody {
      display: block;
      overflow-x: auto;
      white-space: nowrap;
    }
    
    .flip-table tr {
      display: inline-block;
      vertical-align: top;
    }
    
    .flip-table th,
    .flip-table td {
      display: block;
    }
  }
</style>
Warning: When converting tables to cards or lists on mobile, ensure you maintain semantic HTML and add appropriate ARIA attributes. Use data-label attributes to preserve context when hiding headers.

Section 9 Key Takeaways

  • Use <thead>, <tbody>, and <tfoot> for proper table structure and accessibility
  • Always include scope attribute on <th> elements (scope="col" or scope="row")
  • Use <caption> for table titles - essential for screen readers
  • When using colspan or rowspan, ensure row/column totals remain consistent
  • Add aria-sort attributes to sortable column headers
  • For responsive tables, use data-label attributes when stacking cells on mobile
  • Wrap tables in <div style="overflow-x: auto"> for horizontal scrolling on small screens
  • The summary attribute is deprecated - use <caption> or aria-describedby instead