Complex Tables: Styling & Accessibility
Lesson 42 โข Advanced Track
By the end, you'll build data tables that look professional, work on a phone, and read perfectly to a screen reader.
What You'll Learn
- Structure tables with caption, thead, tbody, and tfoot the right way
- Add scope="col" / scope="row" so screen readers can navigate the data
- Style tables with zebra striping, row hover, and status badges
- Pin headers in place while you scroll using position: sticky
- Make wide tables fit a phone with overflow scroll and stacked cards
- Merge cells with colspan and rowspan for multi-level reports
Before you start: you should be comfortable with selectors, the box model, and pseudo-classes from CSS Basics & Selectors and Modern Layouts. This lesson builds directly on those.
๐ก Real-World Analogy
A well-built HTML table is a spreadsheet in disguise. The <caption> is the sheet's title tab, <thead> is the frozen header row, <tbody> is the grid of data, and <tfoot> is the totals row at the bottom. Strip that structure away and a screen reader is left reading a spreadsheet with every column label deleted โ the numbers are there, but nobody can tell what they mean.
Understanding Table Structure
HTML tables have a rich semantic structure most developers underuse. A <table> should contain three sections: <thead> for header rows, <tbody> for data rows, and optionally <tfoot> for summary or total rows. The <caption> element gives the table a title that a screen reader announces before reading any cells.
For accessibility, every <th> needs a scope attribute. Use scope="col" in your header for column labels, and scope="row" on the first cell of each data row (a name, a date, a category) so the assistive technology can link every value back to both its column and its row.
The hardest part is responsiveness. Tables are wide, horizontal things that don't fit narrow phones. The fix is either a scroll wrapper (overflow-x: auto) or the data-label pattern: on mobile, hide the header, turn each row into a card, and use td::before with content: attr(data-label) to reprint the column name beside each value. You'll build both below.
1. Worked Example โ A Styled, Accessible Table
Read this one closely โ it's the full pattern. It uses zebra striping with :nth-child(even), a tr:hover highlight, status badges, right-aligned monospace numbers, and a complete caption / thead / tbody / tfoot structure with scope on every header.
Styled Data Table with Status Badges
<!DOCTYPE html>
<html><head><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
/* Wrapper enables horizontal scroll on narrow screens */
.table-wrapper { overflow-x: auto; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
table { width: 100%; border-collapse: collapse; background: white; } /* collapse = single shared borders */
caption { padding: 16px; font-weight: 700; font-size: 1.1rem; text-align: left; background: #1976D2; color: white; }
/* sticky keeps the he
...๐ฏ Your Turn โ Zebra Striping & Hover
This roster table is plain and hard to scan. Fill in the two ___ blanks in the CSS to add zebra striping and a hover highlight. The instructions and expected result are in the comments.
Your Turn: Zebra + Hover
<!DOCTYPE html>
<html><head><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
caption { padding: 14px; font-weight: 700; text-align: left; background: #1976D2; color: white; }
thead th { background: #E3F2FD; color: #1565C0; padding: 12px 16px; text-align: left; }
tbody td, tbody th { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; text-align: left; }
/* ๐ฏ YOUR TU
...2. Making Tables Responsive
On mobile, this table transforms each row into a stacked card. Each <td> carries a data-label attribute, and a ::before pseudo-element reprints that label so the data still makes sense once the <thead> is hidden. Resize the preview below 600px to watch it switch.
Responsive Table with data-label
<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1"><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
h1 { color: #1565C0; margin-bottom: 8px; }
.subtitle { color: #999; margin-bottom: 24px; }
.table-wrapper { overflow-x: auto; border-radius: 12px; border: 1px solid #e0e0e0; }
table { width: 100%; border-collapse: collapse; }
thead th { background: #f5f5f5; padding: 12px 16px; text-align: left; font-size: 0.85rem; color: #666; text-tr
...๐ฏ Your Turn โ Finish the Stacked Cards
The mobile CSS is already written, but the labels are blank because the cells aren't labelled. Add a data-label="..." to every <td> so the column name appears on small screens. Follow the example comment in the code.
Your Turn: Stacked Cards
<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1"><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
table { width: 100%; border-collapse: collapse; }
thead th { background: #f5f5f5; padding: 12px 16px; text-align: left; }
tbody td { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
/* ๐ฏ YOUR TURN โ finish the mobile card pattern.
The CSS is written, but the labels won't appear until you label the cells. */
@media (max-wid
...3. Sticky Headers for Long Tables
When a table is long, the header scrolls out of view and the data becomes meaningless. position: sticky; top: 0; on the <th> elements pins them to the top of a scrollable container. This example also shows inline progress bars built right inside the cells.
Sticky Header Table with Progress Bars
<!DOCTYPE html>
<html><head><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
h1 { color: #1565C0; margin-bottom: 16px; }
/* The scroll container: fixed height + overflow makes sticky work */
.scroll-table { max-height: 300px; overflow-y: auto; border-radius: 12px; border: 1px solid #e0e0e0; }
table { width: 100%; border-collapse: collapse; }
/* sticky + top:0 pins the header to the top of .scroll-table while you scroll */
thead th { position: sticky; top: 0; background: #1976D
...4. Merging Cells with colspan & rowspan
Complex reports need merged cells. colspan merges horizontally across columns; rowspan merges vertically across rows. They power financial reports, schedules, and grouped data displays.
colspan & rowspan Financial Report
<!DOCTYPE html>
<html><head><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
h1 { color: #1565C0; margin-bottom: 16px; }
table { width: 100%; border-collapse: collapse; border: 2px solid #1976D2; }
th, td { padding: 12px 16px; border: 1px solid #ddd; text-align: center; }
th { background: #E3F2FD; color: #1565C0; font-size: 0.85rem; }
.group-header { background: #1976D2; color: white; font-weight: 700; }
.row-header { background: #f5f5f5; font-weight: 600; text-align: left; }
...When to Use Tables
- DO use tables for: tabular data โ financial reports, comparison charts, schedules, employee directories, product specs, analytics dashboards.
- DON'T use tables for: page layout. That's what CSS Grid and Flexbox are for. Tables-for-layout is a 1990s anti-pattern that breaks accessibility and responsiveness.
- Consider alternatives when: data is simple (1โ2 columns) โ a definition list (
<dl>) may be more semantic. On mobile, card layouts often read better than tables.
๐ฏ Mini-Challenge โ Build It From Scratch
Support is faded now โ this starter has only a comment outline, no filled-in code. Build a styled, accessible Monthly Budget table from the brief in the comments. Use everything from this lesson: structure, scope, zebra striping, hover, and right-aligned numbers.
Mini-Challenge: Monthly Budget Table
<!DOCTYPE html>
<html><head><style>
body { font-family: system-ui, sans-serif; padding: 24px; }
/* ๐ฏ MINI-CHALLENGE: Build a styled, accessible "Monthly Budget" table.
Support has faded โ only this outline is given. Write the HTML + CSS yourself.
1. Wrap the table in a div with overflow-x: auto (mobile scroll safety net).
2. Give the table a <caption> reading "Monthly Budget".
3. <thead> with three column headers: Category, Budget, Spent (use scope="col").
4. <tbody> with at le
...Common Errors
- Sticky header won't stick.
position: stickydoes nothing without atopvalue and a scrolling ancestor. Put the table in a container with a fixedmax-heightandoverflow-y: auto, and addtop: 0to theth. - Double borders and gaps between cells. You forgot
border-collapse: collapseon thetable. Add it so adjacent borders merge into one. - data-label shows nothing on mobile. The
::beforereadsattr(data-label), so eachtdmust actually have adata-label="..."attribute โ a missing one prints an empty label. - Screen reader reads numbers with no context. You're missing
scope. Addscope="col"to header cells andscope="row"to each row's lead cell. - Table overflows and breaks the page on phones. Wrap it in a
divwithoverflow-x: autoso it scrolls sideways instead of bursting its container.
๐ Quick Reference
| Element / Property | What it does |
|---|---|
| <caption> | Title announced before the table by screen readers |
| <thead> / <tbody> / <tfoot> | Group header, data, and summary rows |
| scope="col" / "row" | Links a <th> to its column or row for accessibility |
| border-collapse: collapse | Merges cell borders into single shared lines |
| tr:nth-child(even) | Selects every second row for zebra striping |
| tr:hover | Highlights the row under the cursor |
| position: sticky; top: 0 | Pins the header while the container scrolls |
| overflow-x: auto | Lets a wide table scroll sideways on mobile |
| content: attr(data-label) | Reprints the column name in the stacked-card pattern |
| colspan / rowspan | Merge cells horizontally / vertically |
Frequently Asked Questions
Why should I use thead, tbody, and tfoot instead of just rows?
These section tags tell the browser and screen readers what each part of the table means. thead marks the header rows, tbody the data, and tfoot the totals. They also unlock features you can't get otherwise: position: sticky headers work cleanly with thead, and a screen reader can announce 'table header' versus 'table body' so a blind user knows where they are.
What's the difference between scope="col" and scope="row"?
scope="col" on a <th> says 'this header labels the whole column below it' โ use it in your thead. scope="row" says 'this header labels the row to its right' โ use it on the first cell of a data row, like a person's name or a date. Scope is how a screen reader links each data cell back to the right header when reading across.
How do I make a wide table work on a phone?
Two patterns. The simplest is to wrap the table in a div with overflow-x: auto so it scrolls sideways. The fancier one is the data-label stacked-card pattern: with a @media query you hide the thead and turn each row into a card, then a td::before with content: attr(data-label) reprints the column name next to each value. Pick scroll for dense data, cards for readability.
Why isn't my sticky header sticking?
position: sticky needs three things to work. The element needs position: sticky AND a top value (usually top: 0). The scroll has to happen on an ancestor โ put your table inside a container with a fixed max-height and overflow-y: auto. And no parent between the th and the scroll container can have overflow: hidden, or sticky silently stops working.
When should I NOT use a table?
Never use a table for page layout โ that's a 1990s anti-pattern that breaks on mobile and confuses screen readers; use CSS Grid or Flexbox instead. Only use <table> for genuinely tabular data: things with rows and columns that relate to shared headers, like reports, schedules, and price comparisons.
๐ Lesson Complete
- โ
Use
<caption>,<thead>,<tbody>,<tfoot>for semantic structure - โ
Add
scope="col"andscope="row"to every<th>for accessibility - โ
Zebra striping with
:nth-child(even)plustr:hovermakes data easy to scan - โ
position: sticky; top: 0pins headers in a scrollable container - โ
data-label+::beforeturn rows into mobile cards;overflow-x: autois the simple fallback - โ
colspanmerges horizontally,rowspanmerges vertically - โ Next lesson: Print Styles for export-friendly layouts
Sign up for free to track which lessons you've completed and get learning reminders.