← Back to the Tables Reference
Lesson

HTML Tables

Tables organize real data into rows and columns — prices, schedules, stats, comparisons. This visual lesson covers when to use one, the anatomy, building step by step, headers & captions for accessibility, spanning cells, styling, and making tables work on phones. Live tables throughout.

Step 1

When to use a table (and when not)

Use a <table> for tabular data — information that has a natural relationship across rows and columns, where the row and column together give a cell its meaning.

Quick test: would the data still make sense as a spreadsheet? If yes → table. If you're only using it to position boxes on the page → use CSS layout instead.
Step 2

The anatomy of a table

A table nests a few elements. Here's the family tree:

<table> the whole table <caption> the table's title <thead> header row group <tr> table row <th> header cell <tbody> the body rows <tr> <td> data cell <tfoot> footer row group (totals) </table>
Two cell types: <th> = header cell (bold & centered by default, and announced as a header), <td> = data cell. Rows are <tr> (table row).
Step 3

Build one, step by step

Here's a minimal table — code on top, the rendered result below.

<table>
  <thead>
    <tr><th>Item</th><th>Price</th></tr>
  </thead>
  <tbody>
    <tr><td>Coffee</td><td>$3</td></tr>
    <tr><td>Tea</td><td>$2</td></tr>
  </tbody>
</table>
ItemPrice
Coffee$3
Tea$2
Step 4

Header cells & scope

Headers can run across the top (columns) and down the side (rows). Add scope so screen readers announce each data cell with its headers — e.g. “Tuesday, HTML, 2 hours.”

<tr>
  <th scope="col">Week</th>   column header
  <th scope="col">HTML</th>
</tr>
<tr>
  <th scope="row">Week 1</th>  row header
  <td>3 hrs</td>
</tr>
WeekHTMLCSS
Week 13 hrs2 hrs
Week 22 hrs4 hrs
This is the #1 table accessibility fix. Without scope, a screen reader just reads numbers; with it, every cell is announced in context.
Step 5

Give it a <caption>

The <caption> is the table's title. It must be the first child of <table>, and it's read aloud first so users know what the table is about before diving into cells.

<table>
  <caption>Weekly study hours</caption></table>
Weekly study hours
WeekHours
Week 15
Week 26
Step 6

Spanning cells: colspan & rowspan

A cell can stretch across multiple columns (colspan) or down multiple rows (rowspan) — for grouped headers or merged labels.

<th colspan="2">Weekend</th>     one cell, two columns wide
<td rowspan="2">Closed</td>     one cell, two rows tall
DayHours
 OpenClose
Mon95
Sat–SunClosed
Watch your counts. Every row must still add up to the same number of columns once spans are counted — a mismatched colspan is the classic reason a table looks broken.
Step 7

<thead>, <tbody>, <tfoot>

These group the rows by role. Beyond meaning, they enable nice touches: a sticky header when the body scrolls, and a totals row in the foot.

Q1 sales
MonthSales
Jan$1,200
Feb$1,500
Mar$1,800
Total$4,500
Bonus: thead th { position: sticky; top: 0; } keeps the header visible while a long <tbody> scrolls.
Step 8

Styling: borders, zebra stripes, hover

A few CSS rules turn a default table into a readable one. The key one is border-collapse: collapse so borders don't double up.

table { border-collapse: collapse; width: 100%; }
th, td { padding: 10px 12px; text-align: left; }
tbody tr:nth-child(even) { background: #eef4fb; }   zebra stripes
tbody tr:hover { background: #dbeafe; }            row highlight
NameRoleCity
Ada LovelaceEngineerLondon
Grace HopperEngineerNew York
Mae JemisonScientistDecatur
Katherine JohnsonMathematicianHampton
Right-align numbers (text-align: right) so they line up by place value, and keep generous padding — cramped tables are hard to scan.
Step 9

Tables on small screens

Wide tables overflow a phone. The simplest, most reliable fix: wrap the table in a scrolling container so it scrolls sideways instead of breaking the layout.

<div class="table-wrap">
  <table></table>
</div>

.table-wrap { overflow-x: auto; }   scroll instead of overflow
PlanPriceStorageUsersSupport
Free$01 GB1Email
Pro$12100 GB5Priority
Team$491 TBUnlimited24/7

↑ On a narrow screen, swipe this table sideways — it scrolls instead of squishing.

For complex data on mobile, the other approach is to restack each row as a little card with CSS (using data-label attributes). The scroll wrapper is simpler and works for almost everything.
Step 10

Putting it together: one big table

This workshop schedule uses nearly everything at once — grouped headers with colspan, sessions that merge down two time slots with rowspan, <thead>/<tbody>/<tfoot>, scope on every header, and CSS zebra striping with :nth-child(). Rendered table first, then the HTML and CSS — each with its own copy button.

Fall Workshop Schedule
Time Web Track Design Track
HTML / CSS JavaScript UX Figma
8:00Check-in
9:00 HTML Basics Intro JSWireframesFigma 101
10:00 JS FunctionsUX ResearchComponents
11:00 FlexboxThe DOMPrototypingHandoff
12:00Lunch
1:00 CSS GridAPIsTestingTokens
2:00 Project Lab ProjectProjectProject
3:00 ReviewReviewReview
4:00 Office HoursOpen labOpen labOpen lab
Doors close at 5:00 - see you tomorrow

HTML

<table class="sched">
  <caption>Fall Workshop Schedule</caption>
  <thead>
    <tr>
      <th scope="col" rowspan="2">Time</th>
      <th scope="col" colspan="2">Web Track</th>      <!-- one header, 2 columns -->
      <th scope="col" colspan="2">Design Track</th>
    </tr>
    <tr>
      <th scope="col">HTML / CSS</th><th scope="col">JavaScript</th>
      <th scope="col">UX</th><th scope="col">Figma</th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">8:00</th><td class="full" colspan="4">Check-in</td></tr>
    <tr>
      <th scope="row">9:00</th>
      <td rowspan="2">HTML Basics</td>                 <!-- spans 9:00 + 10:00 -->
      <td>Intro JS</td><td>Wireframes</td><td>Figma 101</td>
    </tr>
    <tr>
      <th scope="row">10:00</th>
      <td>JS Functions</td><td>UX Research</td><td>Components</td>
    </tr>
    <tr>
      <th scope="row">11:00</th>
      <td>Flexbox</td><td>The DOM</td><td>Prototyping</td><td>Handoff</td>
    </tr>
    <tr><th scope="row">12:00</th><td class="full" colspan="4">Lunch</td></tr>
    <tr>
      <th scope="row">1:00</th>
      <td>CSS Grid</td><td>APIs</td><td>Testing</td><td>Tokens</td>
    </tr>
    <tr>
      <th scope="row">2:00</th>
      <td rowspan="2">Project Lab</td>                 <!-- spans 2:00 + 3:00 -->
      <td>Project</td><td>Project</td><td>Project</td>
    </tr>
    <tr>
      <th scope="row">3:00</th>
      <td>Review</td><td>Review</td><td>Review</td>
    </tr>
    <tr>
      <th scope="row">4:00</th>
      <td>Office Hours</td><td>Open lab</td><td>Open lab</td><td>Open lab</td>
    </tr>
  </tbody>
  <tfoot>
    <tr><td colspan="5">Doors close at 5:00 - see you tomorrow</td></tr>
  </tfoot>
</table>

CSS

.sched { border-collapse: collapse; width: 100%; font-size: .85rem; }
.sched caption { text-align: left; font-weight: 700; padding-bottom: 8px; }
.sched th,
.sched td { border: 1px solid #cbd5e1; padding: 8px 10px; text-align: left; }
.sched thead th { background: #0f172a; color: #fff; }   /* dark column headers */
.sched tbody th { background: #eef2f6; }                /* row (time) labels  */

/* Zebra striping — shade every even body row */
.sched tbody tr:nth-child(even) { background: #f1f5f9; }

/* Spotlight one specific row — here the 9th */
.sched tbody tr:nth-child(9) td { background: #dbeafe; }

.sched .full    { text-align: center; font-style: italic; background: #fff7ed; }
.sched tfoot td { background: #0f172a; color: #e5e7eb; text-align: center; }
What's happening: colspan stretches the “Web Track” header and the Check-in / Lunch rows across columns; rowspan makes “HTML Basics” and “Project Lab” cover two time slots. The CSS stripes every even row with :nth-child(even) and spotlights just the 9th row with :nth-child(9) — and the whole table sits in an overflow-x:auto wrapper so it scrolls on phones.
Step 11

Table checklist

Where next

Keep going