A Guide to Styling Tables

madsstoumann

Mads Stoumann

Posted on January 28, 2024

A Guide to Styling Tables

I've recently noticed a small paradox: Many years ago – before CSS grid — we used <table>s to simulate grid layouts. Now that we have grid layouts, we use them to simulate tables! Which is wrong. Tables are for tabular data; and it doesn't make sense to present tabular data in a bunch of <div>s.

The reason for this malpractice might be because tables can be a bit tricky to style, and that most CSS frameworks use border-collapse: collapse for default table styling. As we'll see in this tutorial, collapsed borders are not always useful for table styling.

Let's look into the elements of a <table>, and then how to structure and style them.

Elements

Besides the <table>-element itself, you only need these 3 tags to do a basic table:

Tag Description
td Table Data Cell
th Table Header Cell
tr Table Row

Example:



<table>
  <tr><th>Header</th></tr>
  <tr><td>Content</td></tr>
</table>


Enter fullscreen mode Exit fullscreen mode

However, to structure the table better, we can encapsulate the rows in:

Tag Description
thead Table Header
tbody Table Body
tfoot Table Footer

Finally, we can add a <caption> to the table, and define columns in <col>-tags within a <colgroup>.

Example:



<table>
  <caption>Super Heroes</caption>
  <colgroup><col><col><col><col></colgroup>
  <thead>
    <tr><th>First Name</th><th>Last Name</th><th>Known As</th><th>Place</th></tr>
  </thead>
  <tbody>
    <tr><td>Bruce</td><td>Wayne</td><td>Batman</td><td>Gotham City</td></tr>
    <tr><td>Clark</td><td>Kent</td><td>Superman</td><td>Metropolis</td></tr>
    <tr><td>Tony</td><td>Stark</td><td>Iron Man</td><td>Malibu</td></tr>
    <tr><td>Peter</td><td>Parker</td><td>Spider-Man</td><td>New York City</td></tr>
    <tr><td>Matt</td><td>Murdock</td><td>Daredevil</td><td>New York City</td></tr>
  </tbody>
</table>


Enter fullscreen mode Exit fullscreen mode

Without any styles, your browser will render this:

Basic Tables Browser Styles

The default user-agent-styles are:



table {
  border-collapse: separate;
  text-indent: initial;
  border-spacing: 2px;
}


Enter fullscreen mode Exit fullscreen mode

Now, if we add a super-simple rule:



:is(td,th) {
  border-style: solid;
}


Enter fullscreen mode Exit fullscreen mode

We get:

Basic Table with solid border

Notice the separate borders. It doesn't look too nice ...

So, just to understand the popularity of collapsed borders (as well as a better font!), if we simply add:



table {
  border-collapse: collapse;
  font-family: system-ui;
}


Enter fullscreen mode Exit fullscreen mode

... we get:

border-collapse set to collapse

If we then add padding: .5ch 1ch to our :is(td,th)-selector and margin-block: 1rlh to <caption>, we get:

Basic Table Styles

To recap, all we need to get the above styling, is this:



table {
  border-collapse: collapse;
  font-family: system-ui;
  & caption { margin-block: 1rlh; }
  &:is(td, th) {
    border-style: solid;
    padding: .5ch 1ch;
  }
}


Enter fullscreen mode Exit fullscreen mode

To place the <caption> below the table instead, use:



table {
  caption-side: bottom;
}


Enter fullscreen mode Exit fullscreen mode

Zebra Stripes

To add odd/even zebra-stripes for columns, we can simply style the <col>-tag:



col:nth-of-type(even) { background: #F2F2F2; }


Enter fullscreen mode Exit fullscreen mode

Col Zebra

For rows, it's similar:



tr:nth-of-type(odd) { background: #F2F2F2; }


Enter fullscreen mode Exit fullscreen mode

Zebra Rows


Rounded corners

Rounded corners are a bit tricky. You can't just add border-radius to a <table>, so we have to target the first and last cell of the first and last rows:



th {
  &:first-of-type { border-start-start-radius: .5em }
  &:last-of-type { border-start-end-radius: .5em }
}
tr {
  &:last-of-type {
    & td {
      &:first-of-type { border-end-start-radius: .5em }
      &:last-of-type { border-end-end-radius: .5em }
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

... but still, nothing happens! That's because:

If your table has collapsed borders, you can't add border-radius.

So we'll have to use separate borders, and just mimick collapsed borders:



table {
  border-spacing: 0;
}
:is(td, th) {
  border-block-width: 1px 0;
  border-inline-width: 1px 0;
  &:last-of-type { border-inline-end-width: 1px }
}


Enter fullscreen mode Exit fullscreen mode

And now we have rounded corners:

Rounded corners


Split Columns

Let's keep the separate columns, and use the border-spacing-property to add a gap between columns:



table {
  border-spacing: 2ch 0;
  & :is(td, th) {
    border-inline-width: 1px;
  }
}


Enter fullscreen mode Exit fullscreen mode

Split columns

We can even add border-radius:

Border radius

This is still just a <table>, but much more readable if used as a "comparison table".


Split Rows

For split rows, we just need to update the second part (the y-axis) of the border-spacing-property:



table {
  border-spacing: 0 2ch;
  & :is(td, th) {
    border-block-width: 1px;
  }
}


Enter fullscreen mode Exit fullscreen mode

Split Rows


Hover and Focus

With large tables, it's important to know exactly where you are. For that we need :hover, and — if you're working with a keyboard-navigable table — :focus-visble-styles.

In this example, hover-styles are applied to both <col>, <tr> and <td>:

Table Hover Example

Hovering rows and cells is straightforward:



td:hover {
  background: #666666;
}

tr:hover {
  background: #E6E6E6;
}


Enter fullscreen mode Exit fullscreen mode

Hovering a <col> is a bit more complicated.

You can add a rule:



col:hover {
  background: #E6E6E6;
}


Enter fullscreen mode Exit fullscreen mode

... but it doesn't work. Weirdly, if you select a col-element in Dev Tools and enable :hover for it, it works — but not IRL.

Instead, we need to capture the hovering of cells using :has, and then style the <col>-element:



table {
  &:has(:is(td,th):nth-child(1):hover col:nth-child(1) {
background: #E6E6E6;
}


Enter fullscreen mode Exit fullscreen mode

So, what's going on?

Let's break it down:

If our table has a <td> or a <th> which is the nth-child(1) and it's currently hovered, then select the <col> with the same nth-child-selector, and set it's background.

Phew! ... and you need to repeat this code for each column: nth-child(2), nth-child(3) etc.


Outlines

To show outlines on hover is also straightforward, and the same for cells and rows. You need to deduct the width from the offset:



:is(td, th, tr):hover {
  outline: 2px solid #666;
  outline-offset: -2px;
}


Enter fullscreen mode Exit fullscreen mode

Table Hover: Outlines

Column Outlines

To outline a column is very tricky, but looks nice:

Table Hover: Outline Column

If the cells have a border-widthof 1px, you can set the <col>'s border-width to 2px on hover, but then the whole table shifts.

Álvaro Montoro suggested using background-gradients on <col> to simulate a border, which works fine, if the table cells are transparent.

To make it work with border-radius and keeping whatever background the cells might have, I ended up using a pseudo-element per cell:



:is(td,th) {
  position: relative;
  &::after {
    border-inline: 2px solid transparent;
    border-radius: inherit;
    content: '';
    inset: -2px 0 0 0;
    position: absolute;
  }
}
tr:first-of-type th::after {
  border-block-start: 2px solid transparent;
}
tr:last-of-type td::after {
  border-block-end: 2px solid transparent;
}


Enter fullscreen mode Exit fullscreen mode

... and then, similar to what we did with col-hover, targetting all cells with the same "col-index" on hover:



:has(:is(td,th):nth-child(1):hover :is(td,th):nth-child(1) {
  border-color: #666;
}


Enter fullscreen mode Exit fullscreen mode

Repeat for all columns.


Aligning text

In an old specification, you could add an align-property to the <col>-element. That doesn't work anymore.

Example: You want to center the text in the second column and right-align the text in the fourth column:

Table: Align Text

Instead of adding a class to each cell, we can add a data-attribute per column to the table itself:



<table data-c2="center" data-c4="end">


Enter fullscreen mode Exit fullscreen mode

Then, in CSS:



[data-c2~="center"] tr > *:nth-of-type(2) {
  text-align: center;
}
[data-c4~="end"] tr > *:nth-of-type(4) {
  text-align: end;
}


Enter fullscreen mode Exit fullscreen mode

Repeat for all columns.


Conclusion

And that concludes the guide to table styling.

I didn't cover colspan, rowspan, scope and span. If you want to dive more into these, I suggest reading the MDN page on tables.

Demo

I've made a single CodePen with a bunch of demos here:


Update

In the comments, RioBrewster wrote:

You don't need:
<colgroup><col><col><col><col></colgroup>

You do need: <th scope="col"> for each of the column headers.

Let me answer that with an example. Say you want to highlight the last column. Using <col>, you simply add a class:



<col class="highlight">


Enter fullscreen mode Exit fullscreen mode

In CSS:



.highlight { background-color: HighLight; }


Enter fullscreen mode Exit fullscreen mode

That returns:

Highlight

On the other hand, if you're using:



<th scope="col" class="highlight">...</th>


Enter fullscreen mode Exit fullscreen mode

You get:

Highlight with scope

So that clearly doesn't work. We must add something more.

See MDN's example. They add <td scope="row"> to all the first cells of each row to "highlight the column".

That way, or using a bunch of nth--selectors to highlight a column, is much more work than simply using the <col>-tag.

So, IMO, it's not "either or", but rather "either and".

💖 💪 🙅 🚩
madsstoumann
Mads Stoumann

Posted on January 28, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related