Tables are the right tool for data with rows and columns: price lists, timetables, sports results, invoices and comparisons. Without CSS, though, a table is a cramped grid of text with no lines, no spacing and no way to follow a row across the screen. A few well-chosen properties turn it into something people can scan in seconds.
The core ideas are simple. border-collapse merges double borders into single lines. Padding gives each cell breathing room. Zebra stripes and hover highlights help the eye follow a row. Alignment matters too: numbers line up best on the right, so you can compare their size at a glance.
The hardest part is small screens. A table with six columns can't fit on a 360-pixel phone. In this lesson you'll learn the two common fixes, a scrolling wrapper and a card-style layout, along with sticky headers for long tables. The HTML structure was covered in the HTML tables lesson; here you'll focus on the styling.
Borders and spacing
table { border-collapse: collapse; width: 100%; }
th, td { border-bottom: 1px solid #e5e7eb; padding: 10px 12px; text-align: left; }- By default each cell has its own border with a gap between cells, so borders look doubled.
border-collapse: collapsemerges them into one line. border-spacingsets the gap between cells, but only when borders are not collapsed.- Padding on
thandtdis the single biggest improvement you can make to a table. - Many modern designs use only horizontal lines (
border-bottom) rather than a full grid, which looks lighter and cleaner.
Stripes, hover and alignment
| Technique | CSS |
|---|---|
| Zebra stripes | tbody tr:nth-child(even) { background: #f9fafb; } |
| Hover row | tbody tr:hover { background: #eff6ff; } |
| Right-aligned numbers | td.num { text-align: right; font-variant-numeric: tabular-nums; } |
| Header style | th { background: #f3f4f6; font-weight: 600; } |
| Caption | caption { caption-side: bottom; text-align: left; } |
font-variant-numeric: tabular-nums makes every digit the same width, so columns of prices line up perfectly. vertical-align: top on cells keeps the start of each cell's text level when some cells wrap onto several lines.
Sticky headers and column widths
For long tables, keep the header row visible while scrolling:
thead th { position: sticky; top: 0; background: white; }The header needs a background, or the rows will show through it. To control column widths, use table-layout: fixed with widths on the first row's cells. Fixed layout is also faster for big tables, because the browser doesn't need to read every cell before drawing.
Responsive tables
| Approach | How | Best for |
|---|---|---|
| Scrolling wrapper | Put the table in a div with overflow-x: auto | Wide data tables, comparison tables |
| Stacked cards | Under a breakpoint, make rows display: block and show labels from data-label | Simple lists such as orders |
| Hide columns | Hide less important columns on small screens | Only when that data is truly optional |
The scrolling wrapper is the simplest and keeps the table structure for screen readers. The stacked card approach reads well on phones but needs a label for each cell.
CSS Tables Syntax
Table styling
table { border-collapse: collapse; width: 100%; }
th, td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; }
tbody tr:nth-child(even) { background: #f9fafb; }
tbody tr:hover { background: #eff6ff; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
thead th { position: sticky; top: 0; background: white; }
.table-wrap { overflow-x: auto; }CSS Tables Summary
border-collapse: collapseturns doubled cell borders into single lines.- Add padding to
thandtdand align text left and numbers right. tr:nth-child(even)makes zebra stripes, andtr:hoverhighlights the row under the mouse.position: sticky; top: 0on header cells keeps them visible while scrolling.- Wrap wide tables in an
overflow-x: autobox so phones don't scroll sideways.
CSS Tables Examples
Each example runs in the page. Edit the code and press Run to see what changes.
A clean striped table
<table>
<caption>Monthly sales by region</caption>
<thead>
<tr><th>Region</th><th class="num">Orders</th><th class="num">Revenue</th></tr>
</thead>
<tbody>
<tr><td>North</td><td class="num">1,204</td><td class="num">£18,320.00</td></tr>
<tr><td>South</td><td class="num">987</td><td class="num">£14,115.50</td></tr>
<tr><td>East</td><td class="num">1,511</td><td class="num">£22,904.75</td></tr>
<tr><td>West</td><td class="num">642</td><td class="num">£9,870.20</td></tr>
</tbody>
</table>table { border-collapse: collapse; width: 100%; font-family: sans-serif; font-size: 15px; }
caption { caption-side: bottom; text-align: left; padding-top: 8px; color: #6b7280; font-size: 13px; }
th, td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; }
th { background: #f3f4f6; font-weight: 600; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:nth-child(even) { background: #f9fafb; }
tbody tr:hover { background: #eff6ff; }Line-by-line explanation
- Line 1Collapsed borders and full width.
- Line 2The caption moves below the table as a quiet note.
- Line 4Padding and a line under each row.
- Line 7Numbers align right and every digit has the same width.
- Lines 9 to 10Stripes and a hover highlight help follow a row.
A scrolling table with a sticky header
Scroll inside the table. The header stays put, and on narrow screens it scrolls sideways too.
<div class="wrap" tabindex="0" role="region" aria-label="Train times">
<table>
<thead><tr><th>Train</th><th>From</th><th>To</th><th>Departs</th><th>Arrives</th><th>Platform</th></tr></thead>
<tbody>
<tr><td>1A01</td><td>London</td><td>York</td><td>08:00</td><td>09:52</td><td>4</td></tr>
<tr><td>1A03</td><td>London</td><td>Leeds</td><td>08:33</td><td>10:41</td><td>6</td></tr>
<tr><td>1A05</td><td>London</td><td>Durham</td><td>09:00</td><td>11:48</td><td>2</td></tr>
<tr><td>1A07</td><td>London</td><td>Edinburgh</td><td>09:30</td><td>13:52</td><td>5</td></tr>
<tr><td>1A09</td><td>London</td><td>Newcastle</td><td>10:00</td><td>12:57</td><td>3</td></tr>
<tr><td>1A11</td><td>London</td><td>York</td><td>10:30</td><td>12:21</td><td>4</td></tr>
</tbody>
</table>
</div>.wrap {
max-height: 200px;
overflow: auto;
border: 1px solid #e5e7eb;
border-radius: 10px;
}
table { border-collapse: collapse; min-width: 520px; width: 100%; font: 14px sans-serif; }
th, td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; white-space: nowrap; }
thead th {
position: sticky;
top: 0;
background: #111827;
color: white;
}Line-by-line explanation
- Lines 2 to 3The wrapper scrolls both ways, so the page itself never does.
- Line 8A minimum width stops the columns squashing on phones.
- Lines 12 to 13Header cells stick to the top of the scrolling box.
- Line 14A solid background stops rows showing through the header.
Stacked cards on small screens
Drag the result narrower than 500px to see the rows turn into cards.
<table class="orders">
<thead><tr><th>Order</th><th>Date</th><th>Status</th><th>Total</th></tr></thead>
<tbody>
<tr><td data-label="Order">#1042</td><td data-label="Date">3 May</td><td data-label="Status">Shipped</td><td data-label="Total">£24.00</td></tr>
<tr><td data-label="Order">#1043</td><td data-label="Date">5 May</td><td data-label="Status">Processing</td><td data-label="Total">£58.50</td></tr>
</tbody>
</table>.orders { border-collapse: collapse; width: 100%; font: 15px sans-serif; }
.orders th, .orders td { padding: 10px; border-bottom: 1px solid #e5e7eb; text-align: left; }
@media (max-width: 500px) {
.orders thead { display: none; }
.orders tr { display: block; margin-bottom: 12px; border: 1px solid #e5e7eb; border-radius: 10px; }
.orders td { display: flex; justify-content: space-between; }
.orders td::before { content: attr(data-label); font-weight: 600; }
}Line-by-line explanation
- Line 4The card layout only applies on narrow screens.
- Line 5The header row is hidden because each cell will show its own label.
- Line 6Each row becomes a bordered card.
- Line 8The label comes from the cell's
data-labelattribute.
Common CSS Tables Mistakes
Forgetting border-collapse
td { border: 1px solid #ccc; } /* double lines with gaps */table { border-collapse: collapse; }
td { border: 1px solid #ccc; }Letting a wide table break the page
table { width: 900px; } /* forces the whole page to scroll on phones */.table-wrap { overflow-x: auto; }
table { min-width: 600px; width: 100%; }Sticky header without a background
thead th { position: sticky; top: 0; } /* rows show through */thead th { position: sticky; top: 0; background: white; }CSS Tables in Real Projects
Tables in the wild
Banking apps show transactions in striped tables with right-aligned amounts. Admin dashboards add sticky headers, hover rows and sortable columns. Comparison tables on product sites use scrolling wrappers on mobile, while order histories often switch to cards. GitHub and Stripe both use light horizontal lines rather than full grids for a calm, modern look.
CSS Tables Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Merge the doubled cell borders into single lines.
table {
border-collapse: ;
}
collapse merges adjacent borders.Which selector styles every other row for zebra stripes?
How do you keep a header row visible while scrolling a long table?
Why use font-variant-numeric: tabular-nums in a price column?
The table should have single collapsed borders, 10px cell padding, and right-aligned numbers. Fix it.
<table class="t"><tr><td class="name">Tea</td><td class="num">2.50</td></tr></table>.t { border-collapse: separate; }
.t td { border: 1px solid #ccc; padding: 0; }
.num { text-align: left; }- Borders collapsed
- Cells have 10px padding
- Numbers align right
CSS Tables Quiz
5 questions to check what you learned.
What does padding on td improve most?
- Readability, by giving each cell room
- Page speed
- Borders
- Table width
When does border-spacing have an effect?
- Always
- Only on th
- Only with table-layout: fixed
- Only when border-collapse is separate
What's the simplest way to make a wide table usable on phones?
- Shrink the font to 8px
- Wrap it in a container with overflow-x: auto
- Remove the table
- Use position: absolute
How should numbers in a column be aligned?
- Left
- Centre
- Right
- Justified
What does table-layout: fixed do?
- Sets column widths from the first row, which is faster and more predictable
- Stops the table scrolling
- Makes the table position: fixed
- Hides overflow
CSS Tables Challenge
A price table
- Collapse the borders of
.pricesand make itwidth: 100% - Give
thandtdpadding: 10pxand aborder-bottomof1px solid rgb(229, 231, 235) - Make even rows in
tbodybackground: rgb(249, 250, 251) - Right-align
.numcells - Make header cells sticky at the top
<div style="max-height: 120px; overflow: auto">
<table class="prices">
<thead><tr><th>Item</th><th class="num">Price</th></tr></thead>
<tbody>
<tr class="r1"><td>Coffee</td><td class="num">2.80</td></tr>
<tr class="r2"><td>Tea</td><td class="num">2.20</td></tr>
<tr><td>Juice</td><td class="num">3.10</td></tr>
</tbody>
</table>
</div>th { background: white; }
- Collapsed full-width table
- Padded cells with lines
- Even rows striped
- Numbers right-aligned
- Sticky header