tutorial // Sep 23, 2022

How to Create a Responsive Table with HTML and CSS

How to style an HTML table with CSS to make it responsive to the size of a window or device and prevent its content from breaking your layout.

How to Create a Responsive Table with HTML and CSS

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you run that, we need to install one extra dependency, faker:

Terminal

npm i faker@5.5.3

After that's installed, go ahead and start up your app.

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Adding the basic styles for our table

To add some visual context to our work, to start, we're going to wire up some basic table styles. First, let's open up the file at /ui/pages/index/index.js that was created for us when we ran joystick create <app> above and replace its contents with the following:

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

This gives us a skeleton component we can work with, using Joystick's UI component library, @joystick.js/ui. Next, let's add an HTML <table> element with some tester data.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import faker from 'faker';

const Index = ui.component({
  render: ({ each }) => {
    return `
      <div class="table">
        <table>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Email Address</th>
              <th>Telephone</th>
              <th>Street Address</th>
              <th>Unit Number</th>
              <th>City</th>
              <th>State</th>
              <th>Zip Code</th>
            </tr>
          </thead>
          <tbody>
            ${each([...Array(100)], () => {
              return `
                <tr>
                  <td>${faker.name.firstName()}</td>
                  <td>${faker.name.lastName()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.phone.phoneNumber()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.address.secondaryAddress()}</td>
                  <td>${faker.address.cityName()}</td>
                  <td>${faker.address.state()}</td>
                  <td>${faker.address.zipCode()}</td>
                </tr>
              `;
            })}
          </tbody>
        </table>
      </div>
    `;
  },
});

export default Index;

A few things to point out here. First, notice that on the <div></div> placeholder that we already had from the skeleton code we pasted in, we've added a class attribute set to table. This will come in handy later, serving as the "wrapper" that will allow our table's width to flow properly on small screens (the "responsive" part).

Next, inside of that <div></div>, we've got a plain HTML <table> with a <thead></thead> and <tbody></tbody>. In the header we've added a single row with some header <th></th> tags, and in the <tbody></tbody> we've got something that might look a bit weird.

Here, we're writing the code to dynamically generate a hundred rows of fake data for our table. This is completely optional, but helpful for testing out our styles and adding some context to our work.

What we're doing here is making use of the each() render method that's automatically passed to the render() function on a Joystick component. Here, we use JavaScript string interpolation by passing a call to each() inside of a ${} tag (known as a "template expression" or "template literal"). To that each() function, we pass an array that we want to "loop over," calling the function passed as the second argument for each item looped over.

For the array, we're using a clever trick. Here, we're generating an array of 100 undefined values (we don't care about the contents of the array, we just want to generate 100 placeholders to loop over). To do it, we pass an empty [] array, and inside, write ...Array(100) which says "create an array of 100 items and then 'spread,' or copy the contents of that array into this array (the empty one surrounding it)."

With this, we get an array that looks like [undefined, undefined, undefined, ...]. Again, we don't care about the contents of the array, just that it will loop or run 100 times.

The part we do care about is inside of the function that's called for each loop or iteration of our array. Here, we're returning a string of HTML containing a <tr></tr> table row element with 9 <td></td> tags. Each <td></td>'s contents reflect the corresponding <th></th> in the <thead></thead> above.

In this case, we're creating a table of fake people with email addresses, phone numbers, and mailing addresses. To generate those values, we're utilizing the faker package we installed at the start of the tutorial (make sure you're using version 5.5.3 as the latest version is non-functional). We've imported this at the top of our file as faker and back down in our table, we're calling to the various API methods supported by the library.

Each method generates a specific piece of fake data. Here, the functions we're calling should be fairly self-explanatory (notice that they match the <th></th> column names up top).

That gets us part way. Next, let's add in some CSS to make our table look nice.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import faker from 'faker';

const Index = ui.component({
  css: `
    .table table {
      width: 100%;
      border-collapse: collapse;
    }
    
    .table .table-body-header {
      background: #fafafa;
      font-weight: bold;
      font-size: 15px;
    }
    
    .table .text-right {
      text-align: right;
    }
    
    .table .text-left {
      text-align: left;
    }
    
    .table .text-center {
      text-align: center;
    }
    
    .table table thead th {
      background: #fafafa;
      color: #888;
      font-size: 13px;
      font-weight: bold;
      text-transform: uppercase;
      text-align: left;
      padding: 15px 20px;
      border: 1px solid #eee;
    }
    
    .table table tr th,
    .table table tr td {
      white-space: nowrap;
    }

    .table table tr th {
      border-top: none;
    }

    .table table tr th:first-child,
    .table table tr td:first-child {
      border-left: none;
    }

    .table table tr th:last-child,
    .table table tr td:last-child {
      border-right: none;
    }
    
    .table table tbody td {
      color: #555;
      padding: 15px 20px;
      font-size: 15px;
      border: 1px solid #eee;
    }
    
    .table table tbody tr:last-child td {
      border-bottom: none;
    }
  `,
  render: ({ each }) => {
    return `
      <div class="table">
        <table>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Email Address</th>
              <th>Telephone</th>
              <th>Street Address</th>
              <th>Unit Number</th>
              <th>City</th>
              <th>State</th>
              <th>Zip Code</th>
            </tr>
          </thead>
          <tbody>
            ${each([...Array(100)], () => {
              return `
                <tr>
                  <td>${faker.name.firstName()}</td>
                  <td>${faker.name.lastName()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.phone.phoneNumber()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.address.secondaryAddress()}</td>
                  <td>${faker.address.cityName()}</td>
                  <td>${faker.address.state()}</td>
                  <td>${faker.address.zipCode()}</td>
                </tr>
              `;
            })}
          </tbody>
        </table>
      </div>
    `;
  },
});

export default Index;

Up above our render() function here, we're adding another property css to our component and passing it a string of CSS to apply to the HTML rendered by the render() function.

At this stage, the majority of these styles aren't terribly important except for one. At the very top of the CSS we're setting here, we have a rule for .table table, setting the width to 100% and setting border-collapse to collapse. The first property ensures that the <table></table> tag always expands to 100% of its contents, relative to its parent. The second is purely stylistic: this ensures that the default borders browsers add to a table are removed (we replace them with our own, more tasteful-looking borders in the CSS below this).

The rest of our styles are purely presentational, so feel free to modify these as you see fit for your own design. The tl;dr of what's happening here is we're:

  1. Adding a subtle border to all of the cells.
  2. Giving a light gray #fafafa background to the header cells and making their text bold so they're visually separated from the data.
  3. Adding padding to all cells (the <th> and <td> tags) to give the data more room to breathe.

This gets us most of the way, but now, we need to make this table responsive.

Making it responsive

By responsive, we mean that the table will "respond" to the resizing of its parent container, typically a window or a device (e.g. a smartphone or tablet). Because tables are often quite wide due to the number of columns that they have, they usually get smushed, or, create a horizontal scroll on the page which usually breaks your design.

To get around this, what we need to do is make use of the wrapper <div class="table"></div> that we hinted at earlier, setting some styles on it to ensure it "traps" or limits the <table></table> inside of it from smushing its columns, or, creating a horizontal scroll on the browser.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';
import faker from 'faker';

const Index = ui.component({
  css: `
    .table {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      max-width: 900px;
      height: 500px;
      margin: 10px auto 0;
      border: 1px solid #eee;
      border-radius: 3px;
      box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.05);
    }

    .table table {
      width: 100%;
      border-collapse: collapse;
    }
    
   ...
  `,
  render: ({ each }) => {
    return `
      <div class="table">
        <table>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Email Address</th>
              <th>Telephone</th>
              <th>Street Address</th>
              <th>Unit Number</th>
              <th>City</th>
              <th>State</th>
              <th>Zip Code</th>
            </tr>
          </thead>
          <tbody>
            ${each([...Array(100)], () => {
              return `
                <tr>
                  <td>${faker.name.firstName()}</td>
                  <td>${faker.name.lastName()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.phone.phoneNumber()}</td>
                  <td>${faker.internet.email()}</td>
                  <td>${faker.address.secondaryAddress()}</td>
                  <td>${faker.address.cityName()}</td>
                  <td>${faker.address.state()}</td>
                  <td>${faker.address.zipCode()}</td>
                </tr>
              `;
            })}
          </tbody>
        </table>
      </div>
    `;
  },
});

export default Index;

Here, we're adding a style rule at the very top of our CSS that we had from the previous section that looks like this:

.table {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;

  max-width: 900px;
  height: 500px;

  margin: 10px auto 0;
  border: 1px solid #eee;
  border-radius: 3px;
  box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.05);
}

Here, we've separated the different parts of the rule to help us understand (this isn't necessary in your actual code).

First is the important part: here, we're saying that the .table <div></div> should scroll horizontally when its contents overflow its bounds (remember, we said that the <table><table> element should expand to 100% width which, with our current data, will force an overflow). Just below this, for mobile devices, we set -webkit-overflow-scrolling to touch to ensure that scroll utilizes the "rubber band" effect so the table is easy to scroll.

Second, to demonstrate the scroll/overflow, we're setting a max-width of 900px. This is saying "allow the <div></div> to fill up 100% of the window/device but don't allow it to go wider than 900px." For the height, we're more rigid and just set the height to a fixed 500px. Note: these styles are not necessary for the responsive part, and are purely for example.

Finally, the latest four styles are purely for aesthetics. Here, we're adding some spacing between the table and the window on the top, adding a border to it so we can see the actual "edges" where the table overflows, and then adding a shadow and a rounded corner to polish everything up.

That's it! If we load this up in the browser and resize the window, we should see the wrapper <div></div> adapt its width and then our table being horizontally scrollable on any screen size.

Wrapping up

In this tutorial, we learned how to wire up a responsive HTML table. First, we learned how to set up our table with some basic styles and next, how to add the necessary CSS to the wrapper we placed around it to make it responsive. We also learned how to generate some fake data, using a dynamically generated array with placeholder values.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode