DEV Community

Cover image for A Guide to Styling Tables
Mads Stoumann
Mads Stoumann

Posted on • Edited on

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".

Top comments (26)

Collapse
 
efpage profile image
Eckehard

Thank you for the writeup. Maybe we should have some interactive table wizzard to manage those options.

I recognized that things get even trickier if you want cell selection and a hover effect for tables. Any recommendations for this?

Collapse
 
madsstoumann profile image
Mads Stoumann

You mean selecting the text within a cell or editable cells? You can add a contenteditable attribute to each cell, or contenteditable=plaintext. However, this is easier to control from script. When you click on a cell, you can get the cellIndex of the event.target, and the rowIndex from it's parent.

Collapse
 
efpage profile image
Eckehard • Edited

There are some interactive table generators like this or this, but they do not seem to use the full potential of CSS. There are so many options we can use.

I am also struggling with cell selection by click. Contenteditable makes the content ediable, but often you just want so select a cell. Applying an effect on hovering is simple, but how to show a selection? See this example

Thread Thread
 
madsstoumann profile image
Mads Stoumann • Edited

Still not completely sure what you mean! If you want to add a visual clue to the "active cell being edited", either add and remove a class dynamically from JS, or use td:focus or td:focus-visible in CSS. If you add contenteditable directly on cells instead of the whole table, you can tab through the cells and edit as you like (but better to do in JS!). If you want to select all text on selection, look into Selection and Range.

So:

<table>
  <tr>
    <td contenteditable>cell1</td>
    <td contenteditable>cell2</td>
    <td contenteditable>cell3</td>
    <td contenteditable>cell 4</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

And:

td:focus {
  background: crimson;
}
Enter fullscreen mode Exit fullscreen mode

But again: better to control in JS!

Thread Thread
 
efpage profile image
Eckehard

See this example
If you click on a cell, it stays selected. If I select another cell, the previous selection is removed. But this is a library that is quite heavy and relies on jQuery, so I would like to have a solution with CSS only.

I tried td:focus, td: visited, but neither seems to have any effect. "active" works only while i press the mouse only. Surely I can use Javascript, but as anything else is done in CSS, this is kind of awkward.

Tables might also have some options like:

  • Multiselect
  • range select
  • row select

This are options you usually find on data grids in most programming languages, but HTML does not seem to have any option for this. But how can we prevent to implement all this by hand or - at least - make it pretty lightweuight?

Thread Thread
 
madsstoumann profile image
Mads Stoumann

The example you provided does exactly what I wrote: Adds a class to the cell. You can hack this with contenteditable per cell and :focus, but it's not really recommended. For selecting a row, you could add a checkbox to the first cell per row, and then use CSS:

/* simplified */
tr:has(:checked) { background: hotpink; }
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
efpage profile image
Eckehard

Some Jyvascript may do the trick, but it´s not really handy:

let oc = null, or=null 
bc = (el, col) => el.style.backgroundColor = col

mytable.onclick = (el) => {
  if (oc) {
    bc(oc,null)
    bc(or,null)
  }
  bc(oc = el.target,"red")
  bc(or = oc.parentElement,"silver")
}
Enter fullscreen mode Exit fullscreen mode

see example

Collapse
 
anmolbaranwal profile image
Anmol Baranwal

You can also check out the styled table in my old project.

Image description

I was a beginner at that time, so I may not have been aware of these awesome tips you mentioned. Repository on GitHub

Collapse
 
robole profile image
Rob OLeary

Great info. Would be good to cover making tables responsive (fluid) in a follow-up. If you learn grid first, then tables can feel rigid and uncompromising!

Collapse
 
madsstoumann profile image
Mads Stoumann

Good idea! I was also planning to write an article on how to navigate tables/datagrids with keyboards, following W3C's standard.

Collapse
 
riobrewster profile image
RioBrewster

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

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

And you would do better to make "Known as" the first column, and make that
<tr><th scope="row">Batman</th><td>Bruce</td><td>Wayne</td><td>Gotham City</td></tr>

This is how you make the table accessible to screen reader users.

Collapse
 
madsstoumann profile image
Mads Stoumann

See my reply in the update.

Collapse
 
kanetu profile image
kanetu

This post is so wonderful, turns out I have been doing it wrong ever since, some guys tell me that do not using raw HTML

because of its tricky and difficult to style now you change my mind.
Collapse
 
madsstoumann profile image
Mads Stoumann

Cool — happy to hear that!

Collapse
 
chrisburkssn profile image
Chris Burks

Thanks for sharing. This post is very enlightening about styling tables. Very cool.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
beacamphq profile image
BeacampHQ

I wish I had known this earlier, well no knowledge is wasted. Great guide, btw.

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
ashishk1331 profile image
Ashish Khare😎

Now I feel I need more CSS in my life. Great article!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thanks!

Collapse
 
_ndeyefatoudiop profile image
Ndeye Fatou Diop

Loved the post ! I recently started writing online and appreciate the quality of this article!

Collapse
 
madsstoumann profile image
Mads Stoumann

Thank you!

Collapse
 
stevevail profile image
Steve-Vail

So... what if you want a responsive table?

Collapse
 
madsstoumann profile image
Mads Stoumann

I have to write part 2, I guess

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more