Complex Tables
An accessible data table uses semantic structure — <caption> , <thead> , and <th scope> header cells — so screen readers can announce each cell's row and column, while CSS keeps the table readable and responsive on small screens.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end, you'll build data tables that look professional, work on a phone, and read perfectly to a screen reader.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Practice quiz
Which table section element marks the header rows?
- <tbody>
- <tfoot>
- <thead>
- <caption>
Answer: <thead>. <thead> groups the header rows; <tbody> holds the data and <tfoot> the summary/total rows.
What does scope="col" on a <th> mean?
- This header labels the whole column below it
- This header labels the row to its right
- This cell spans multiple columns
- This is the table caption
Answer: This header labels the whole column below it. scope="col" tells assistive tech the header labels the column beneath it; scope="row" labels the row.
Which CSS declaration merges adjacent cell borders into single shared lines?
- border-merge: on
- border-spacing: 0
- border-style: shared
- border-collapse: collapse
Answer: border-collapse: collapse. border-collapse: collapse merges adjacent borders so you don't get doubled lines and gaps.
Which selector applies zebra striping to every second row?
- tr:hover
- tr:nth-child(even)
- tr:first-child
- tr:checked
Answer: tr:nth-child(even). tr:nth-child(even) selects every second row, the standard way to add zebra striping.
What three things does position: sticky need to pin a table header while scrolling?
- position: sticky, a top value, and a scrolling ancestor
- display: flex, a width, and z-index
- overflow: hidden and a fixed height
- float: top and a margin
Answer: position: sticky, a top value, and a scrolling ancestor. Sticky needs position: sticky plus a top value, and a scrollable ancestor with no overflow:hidden in between.
In the stacked-card responsive pattern, what reprints the column name next to each value?
- the <caption> element
- scope="row"
- content: attr(data-label) on td::before
- a second <thead>
Answer: content: attr(data-label) on td::before. td::before with content: attr(data-label) reprints each column's name once the <thead> is hidden on mobile.
Which is the simplest way to keep a wide table usable on a narrow phone?
- Delete some columns
- Wrap it in a div with overflow-x: auto
- Use display: none on the table
- Set the table width to 100vw
Answer: Wrap it in a div with overflow-x: auto. Wrapping the table in a container with overflow-x: auto lets it scroll sideways instead of breaking the layout.
What does colspan="3" do to a table cell?
- Merges it down across 3 rows
- Repeats it 3 times
- Adds 3px of padding
- Merges it across 3 columns
Answer: Merges it across 3 columns. colspan merges a cell horizontally across the given number of columns; rowspan merges vertically.
Which element gives a table a title that screen readers announce before the cells?
- <title>
- <caption>
- <legend>
- <header>
Answer: <caption>. <caption>, placed inside the table, provides a title announced before the table's contents.
When should you NOT use a <table>?
- For financial reports
- For schedules
- For page layout
- For comparison charts
Answer: For page layout. Tables are for genuinely tabular data; using them for page layout is a 1990s anti-pattern — use Grid or Flexbox.