Responsive table as a card with dynamic headers

Marco Troost
5 min readJun 17, 2021

Data tables are a great way to organise structured data, but they are really only meant for screens with lot’s of (horizontal) space. This document describes a solution where table rows are displayed as individual cards on mobile devices, featuring dynamic headers using a little javascript.

Table rows stacked as individual cards on mobile

Premises:

  • The first table header (upper left), isn’t really necessary on mobile
  • Tables are generated using a regular CMS (like Umbraco), and editors can’t really change that much
  • Table header-data has to stem from the actual html

A word before

I actually adore tables. They’re my first love as a frontend developer. Back in the day (before css really), I didn’t use tables to accommodate data, but I abused its unique, grid-like qualities as a layout-mechanism. Many of you will remember tables-in-tables-in-tables, just to position a rounded corner on an element.

Nowadays, tables are used for what they’re meant for: present data in a logical fashion.

About the design

A data-table is basically a grid with rows that contain data related to a single entity¹. Headers describe the content displayed in columns.

In order to gain insight from a table, users must be able to easily compare its values. A data-table is therefore by default not meant for small screens; there is simply not enough room to horizontally fit meaningful headers onscreen.

An elegant way to compare values in a mobile table is to transform rows into individual cards and stack their column-data within (with corresponding descriptive headers).

Wait a minute, isn’t this design supereasy to code?

Short answer: yes.
It is actually rather simple to achieve the desired result, as Chris Coyier has demonstrated in his article about responsive tables.

Example #1: pseudoselectors

We could for instance label every cell’s content (except the first one) using the :before pseudoselector.

tbody {
td {
&:nth-child(n+2) {
&:before {
color: #C0C0C0;
display: inline-block;
line-height: 24px;
margin: 0 0 8px;
@media (min-width: 800px) {
display: none;
}
}
}

&:nth-child(2) {
&:before {
content: 'Internetsnelheid tot 1 juli'
}
}

&:nth-child(3) {
&:before {
content: 'Internetsnelheid vanaf 1 juli'
}
}

}
}

Watch it on codepen

This approach will work excellently if there are a finite amount of columns, and that you are sure that headers won’t change over time.

The downside of this technique is that u won’t know how many columns will be added, removed or edited in future html. The content in the CSS would then be out of sync with the actual DOM anyway.

Example #2: Add a little html

Another technique is to simply add extra span’s for every cell in the table, that u can hide using css.

<tbody>
<tr>
<td>TV & Internet Budget</td>
<td>
<span>Internetsnelheid tot 1 juli</span>
<ul>
<li>50 Mbps downloadsnelheid</li>
<li>5 Mbps uploadsnelheid</li>
</ul>
</td>
<td>
<span>Internetsnelheid tot 1 juli</span>
<ul>
<li>75 Mbps downloadsnelheid</li>
<li>10 Mbps uploadsnelheid</li>
</ul>
</td>
</tr>
</tbody>

Watch it on codepen

This solution isn’t really viable. It will not only create a semantic mess, but it’ll introduce a world of pain trying to manage this table in a content management system.

Shortcomings

The shortcoming of these approaches is that we would either have to add data to the css file (that cannot be redacted through a CMS), or we would have to manipulate the DOM to facilitate our needs.

We want a clean ‘n simple html table with up-to-date header info.
That’s where javascript kicks in.

Javascript for manipulating data

The idea is that we use javascript for collecting the table headers and that we repeatedly prepend them to the their corresponding data columns.

Watch it on codepen

HTML

<table class="responsive-table">
<thead>
<tr>
<th>Product of dienst</th>
<th>Internetsnelheid tot 1 juli</th>
<th>Internetsnelheid vanaf 1 juli</th>
</tr>
</thead>
<tbody>
<tr>
<td>TV & Internet Budget</td>
<td>
<ul>
<li>50 Mbps downloadsnelheid</li>
<li>5 Mbps uploadsnelheid</li>
</ul>
</td>
<td>
<ul>
<li>75 Mbps downloadsnelheid</li>
<li>10 Mbps uploadsnelheid</li>
</ul>
</td>
</tr>
<tr>
<td>TV & Internet Premium</td>
<td>
<ul>
<li>100 Mbps downloadsnelheid</li>
<li>10 Mbps uploadsnelheid</li>
</ul>
</td>
<td>
<ul>
<li>150 Mbps downloadsnelheid</li>
<li>15 Mbps uploadsnelheid</li>
</ul>
</td>
</tr>
</tbody>
</table>

Javascript

if(document.querySelectorAll('.responsive-table')) {
document.querySelectorAll('.responsive-table').forEach((el) => {

let headerArr = [];
el.querySelectorAll('thead th').forEach((th) => {
headerArr.push(th.innerHTML);
});

el.querySelectorAll('tbody tr').forEach((tr) => {
let count = 0;
tr.querySelectorAll('td').forEach((td) => {
let p = document.createElement("p");
p.classList.add('faux-th');
p.innerText = headerArr[count];
td.prepend(p);
count++;
});
});

});
}

Javascript explained

At first we loop through the document to see if there are any responsive tables. For the sake of this tutorial, these tables are given the class “.responsive-table”.

if(document.querySelectorAll('.responsive-table')) {
document.querySelectorAll('.responsive-table').forEach((el) => {

});
}

Then we scan each of those tables and create an array filled with the table-headers that we can use later on.

let headerArr = [];
el.querySelectorAll('thead th').forEach((th) => {
headerArr.push(th.innerHTML);
});

Now that we have the table headers, we can prepend them as a paragraph to the columns in the table body. Looping through each row, we use a counter to collect the correct index in the header-array.

el.querySelectorAll('tbody tr').forEach((tr) => {
let count = 0;
tr.querySelectorAll('td').forEach((td) => {
let p = document.createElement("p");
p.classList.add('faux-th');
p.innerText = headerArr[count];
td.prepend(p);
count++;
});
});

Styling

Since every table header is prepended to each column, we need to hide them for larger screens. This can be done using javascript, but i thought it is easier (for readability purposes) to hide them using css. Each paragraph is therefore given a class ‘.faux-th’ that we can style to our liking.

The table-rows themselves need to be given another display mode for mobile as well. We’ll transform the rows into flexboxes, with its direction set to ‘column’.

SCSS

.responsive-table {
width: 100%;
border-collapse: collapse;

th, td {
text-align: left;
@media (min-width: 800px) {
border: 1px solid #C0C0C0;
}
}

thead {
tr {
display: none;
@media (min-width: 800px) {
display: table-row;
}
}

}

tr {
display: flex;
flex-direction: column;
margin: 0 0 16px;
border: 1px solid #C0C0C0;
@media (min-width: 800px) {
display: table-row;
border: 0;
margin:0;
}
}

th {
padding: 12px;
}

td {
border-top: 1px solid #C0C0C0;
padding: 44px 12px 16px;
position: relative;
@media (min-width: 800px) {
padding: 12px;
border-top: 0;
}

&:first-child {
font-weight: 700;
border-top: 0;
padding: 16px 12px;

.faux-th {
display: none;
}
}

ul {
margin:0;
padding: 0 0 0 16px;
}
}

.faux-th {
height: 36px;
display: flex;
align-items: flex-end;
position: absolute;
top: 0;
font-size: 15px;
margin: 0;
color: #C0C0C0;
@media screen and (min-width: 800px) {
display: none;
}
}
}

Wrap up

There is no need anymore to (mis)use tables for layout purposes. In my view, tables do not really fit a mobile first approach either.

One can’t discard tables however. Content editors will use them, and it is our job to make tables look good on any device. The solution presented here is one just one of many. Please checkout the examples given by Michał Jarosz for design inspiration.

--

--

Marco Troost

I’m an experienced UX Designer / Front-end developer in the South West of Holland with over 20 years experience building websites.