Complete CSS Course Module 5: Beginner: Styling Common Elements

Styling Tables with CSS

  • Beginner
  • Lesson 5.3
  • 4 min read
  • 22 of 77 in the course

In short

Styling tables with CSS turns plain data into something easy to read: add borders, padding, zebra stripes, hover rows, a sticky header and scrolling on small screens. Why do default HTML tables look so plain? Browsers show a table with no borders, very little space and small gaps between cells.

Styling tables with CSS turns plain data into something easy to read: add borders, padding, zebra stripes, hover rows, a sticky header and scrolling on small screens.

Why do default HTML tables look so plain?

Browsers show a table with no borders, very little space and small gaps between cells. That is fine for the browser, but hard for people to read. With a few CSS rules you can make a clear, modern table. Let's start with a small price table and improve it step by step.

<table class="plans">
  <caption>Monthly plans</caption>
  <thead>
    <tr>
      <th>Plan</th>
      <th>Price</th>
      <th>Lessons</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Free</td><td>$0</td><td>20</td></tr>
    <tr><td>Student</td><td>$5</td><td>200</td></tr>
    <tr><td>Pro</td><td>$12</td><td>Unlimited</td></tr>
    <tr><td>Team</td><td>$40</td><td>Unlimited</td></tr>
  </tbody>
</table>

Table borders and border-collapse

If you add a border to each cell, you'll see double lines. That is because every cell has its own border, and there is a gap between cells. The property border-collapse: collapse merges neighbouring borders into one single line, like walls shared by two rooms.

.plans {
  border-collapse: collapse;
  width: 100%;
  font-family: system-ui, sans-serif;
}

.plans th,
.plans td {
  border: 1px solid #d1d5db;
  padding: 10px 14px;
  text-align: left;
}

.plans th {
  background: #1e3a8a;
  color: white;
}

Now the table fills the width of its container, every cell has one thin grey border and comfortable padding, and the header row is dark blue with white text. Padding is the single most important change: it gives the text room to breathe.

Tip: Numbers are easier to compare when they line up on the right. Add a class like .num { text-align: right; } to price and count columns.

Zebra stripes and hover rows

Long tables are easier to follow when every second row has a light background. These are called zebra stripes. You make them with the :nth-child() pseudo-class. A hover color helps users follow one row with their eyes.

.plans tbody tr:nth-child(even) {
  background: #f3f4f6;
}

.plans tbody tr:hover {
  background: #dbeafe;
}

.plans caption {
  caption-side: top;
  text-align: left;
  font-weight: bold;
  font-size: 1.2rem;
  padding-bottom: 8px;
}

Rows 2 and 4 of the body are now light grey. When you move the mouse over any row, it turns light blue. The <caption> is the table's title; here it sits on the top-left in bold. You could move it under the table with caption-side: bottom.

Common mistake: Writing tr:nth-child(even) without tbody. The header row also counts as a row, so your stripes may land in the wrong place. Select tbody tr to stripe only the data rows.

How to make a sticky table header

When a table is long, users scroll down and forget what each column means. A sticky header stays at the top while the rows scroll under it. You use position: sticky on the <th> cells.

.plans thead th {
  position: sticky;
  top: 0;
  z-index: 1;
}

top: 0 means "stick when you reach the top of the scrolling area". The header needs a background color (we gave it dark blue), otherwise the rows would show through it. z-index: 1 keeps it above the rows. You'll learn more about position and z-index in the Positioning module.

Responsive tables: a scrolling wrapper

Tables don't shrink well. On a phone, a wide table can push the whole page sideways. The simple, reliable fix is to put the table inside a wrapper <div> that scrolls horizontally on its own.

<div class="table-wrap">
  <table class="plans">
    <!-- the same table as above -->
  </table>
</div>
.table-wrap {
  overflow-x: auto;
  max-height: 400px;
  overflow-y: auto;
  border: 1px solid #d1d5db;
  border-radius: 8px;
}

.plans {
  min-width: 600px;
}

On a big screen nothing changes. On a small phone, the page stays still and only the table area scrolls left and right. min-width: 600px stops the columns from getting squeezed too thin. Because the wrapper also has a max-height and overflow-y: auto, the sticky header now sticks to the top of the wrapper as you scroll inside it.

Note: A sticky header sticks inside its nearest scrolling box. If the wrapper scrolls, the header sticks to the top of the wrapper, not to the top of the browser window.

To see every table-related property, check MDN's border-collapse page.

Practice exercise

Create a table of five students with three columns: Name, Country and Score. Give it a caption, collapsed borders, padding, a colored header, zebra stripes on the body rows, a hover color and right-aligned scores. Then put it in a scrolling wrapper and check it in your browser's phone view.

Hint: add class="num" to the score cells and style .num { text-align: right; }. In DevTools, the device toolbar lets you test a small screen.

Key takeaways

  • border-collapse: collapse turns double cell borders into single lines.
  • Padding on th and td is the biggest readability win.
  • Use tbody tr:nth-child(even) for zebra stripes and tr:hover for hover rows.
  • position: sticky; top: 0 on th keeps the header visible; give it a background.
  • Wrap wide tables in a div with overflow-x: auto so they scroll on phones.