HTML & CSS
Article

Responsive Solutions for Feature Comparison Tables

By Adrian Sandu

Responsive Web Design and tables are not necessarily the best of friends. Many people have researched the situation and a lot of approaches have been devised (some of them were even rounded up in a recent article here on SitePoint). However we are still far away from the perfect solution and the search continues.

While things are still complicated in the generic case, certain specific cases can be treated with a lot more attention. I am talking here about the feature comparison table. We encounter it in many places – when choosing a car and trying to decide what extra options to choose; on web hosting websites when comparing plans and features; on any membership-based portal that lets you decide what features you need to receive in exchange for your money.

Because this kind of table has a relatively stable and consistent structure, it is possible to coax a better behavior when displayed on small screens.

Anatomy of a Feature Comparison Table

The classic comparison table brings together at least three products (displayed in columns) while the features are displayed on rows below. In the traditional structure, the first cell of each row has the name of the feature, while the cells under each product have a checkmark or some other symbol, showing whether that feature belongs to the product or not. We can find great examples of this classic structure: here, here, and here

Based on these examples, we can summarize the structure of a comparison table with the following code:

<table>
  <thead>
    <tr>
      <th>&nbsp;</th>
      <th>Product 1</th>
      <th>Product 2</th>
      <th>Product 3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Feature 1</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
    </tr>
    <tr>
      <td>Feature 2</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
    </tr>
    <tr>
      <td>Feature 3</td>
      <td>&mdash;</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
    </tr>
    <tr>
      <td>Feature 4</td>
      <td>&mdash;</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
    </tr>
  </tbody>
</table>

It is easy to identify the elements mentioned earlier: the product names, the feature names, and the marks that show whether the feature is present or not. Note that the &#10004; code represents a checkmark (✔) character.

We now arrive at the root of the problem. In order for the table to maintain optimum effectiveness at low screen widths, a few conditions must be fulfilled:

  • The user must be able to easily differentiate the products;
  • The features must be easily identifiable; and
  • It must be clear if a feature for a product is present or not.

The best way to achieve this result is to shift the cell containing the feature name on top of the other three cells that mark the presence or absence of the feature.

First Solution: Flexbox

How can we get this to happen? One answer is flexbox. If you don’t know what flexbox is or if you need a refresher, you can check out Nick Salloum’s recent article on the topic. The rest of us can dive into the solution.

First we need to make sure our changes happen only on small screens. For this to happen, we target our code using a media query, using the classic width of 768px as a breakpoint:

@media screen and (max-width: 768px) {
  tr {
    display: flex;
    flex-flow: row wrap;
    justify-content: space-around;
  }

  td, th {
    display: block;
    width: 33%;
  }

  th:first-child,
  td:first-child {
    text-align: center;
    background: #efefef;
    width: 100%;
  }

  th:first-child {
    display: none;
  }
}

There are a few important things in this set of rules that make the magic happen:

  • We change the display value for the table row to flex and we tell its children to flow in a row, evenly spaced.
  • Next we direct the cells to adopt display:block to normalize them as ordinary containers (leaving the default value will bring interference from the table rules, especially regarding the size).
  • The next step targets the first cell in each row, making it full width and changing the background color, for added contrast. The flow rules make it stay on top of the other three cells – exactly what we needed.
  • We finish the change by hiding the first th so that there is nothing displayed above the product names.

The demo can be viewed here.

Obviously a solution is valid only as long as it has enough support. According to caniuse.com, support for flexbox is over 80% for the most modern variants and over 93% if we include browser versions that require prefixes or use the previous versions of the rules. Support for IE starts with IE10 (only the 2012 syntax), while IE11 has full support. Because we are mainly interested in support on small screens, we can disregard the lack of support for previous versions of IE. On the mobile front, support starts from Android 4.4 and iOS 7.1. Previous versions require vendor prefixes and don’t support the wrap feature.

You should also provide fallbacks, such as the scrolling div used in Bootstrap. This way the visitors that fall outside the support bracket will still have another alternative for their display.

Second Solution: Extra Markup + ARIA Roles

If a large portion of the browsers you’re going to support lack support for flexbox, there is an alternative. In fact this is the solution I used in a real project in 2013. We will need a bit of extra markup: we will have to add one extra row, duplicating the feature name. While this can appear tedious to do by hand, it can be automated if the information is read from a data source. In the end, the code from our initial example should look like this:

<table>
  <thead>
    <tr>
      <th>&nbsp;</th>
      <th>Product 1</th>
      <th>Product 2</th>
      <th>Product 3</th>
    </tr>
  </thead>
  <tbody>
    <tr class="visible-xs" aria-hidden="true">
      <td>&nbsp;</td>
      <td colspan="3">Feature 1</td>
    </tr>
    <tr>
      <td>Feature 1</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
    </tr>
    <tr class="visible-xs" aria-hidden="true">
      <td>&nbsp;</td>
      <td colspan="3">Feature 2</td>
    </tr>
    <tr>
      <td>Feature 2</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
      <td>&#10004;</td>
    </tr>
    <tr class="visible-xs" aria-hidden="true">
      <td>&nbsp;</td>
      <td colspan="3">Feature 3</td>
    </tr>
    <tr>
      <td>Feature 3</td>
      <td>&mdash;</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
    </tr>
    <tr class="visible-xs" aria-hidden="true">
      <td>&nbsp;</td>
      <td colspan="3">Feature 4</td>
    </tr>
    <tr>
      <td>Feature 4</td>
      <td>&mdash;</td>
      <td>&mdash;</td>
      <td>&#10004;</td>
    </tr>
  </tbody>
</table>

The CSS is also pretty simple:

.visible-xs {
  display: none;
}

@media screen and (max-width: 768px) {
  .visible-xs {
    display: table-row;
  }

  td:first-child,
  th:first-child {
    display: none;
  }
}

We can go one extra step for the sake of accessibility and hide the extra markup from screen readers with aria-hidden="true". This way, those screen reader applications that respect the aria-hidden specification will not read the duplicated content twice.

Here is demo of this second solution.

Conclusion

We found here two ways to make a comparison table truly responsive. Both have their pros and cons. In the end, the selected choice should depend on the specifics of your audience. For most cases, the first option (with the fall-back) should be enough. If you really need to cater to the older versions of Android and iOS, you can deploy the second option. Either way, from now on, your feature comparison tables will look a lot better, no matter the screen size.

  • https://twitter.com/_joseoso Jose Osornio

    I like the Flexbox solution, in the past I’ve used data-* attribute to store the column header text for small screens. Here is an example http://t.co/ai6Axkag0E

  • Ted Drake

    Hi Adrian
    It’s great that you considered accessibility. Could you complete this by adding the scope attribute to your th cells? They should have scope=”col” for the top row. Neglecting this attribute will have inconsistent support for screen readers.

    Also, I prefer to use aria-label=”supported” and aria-label=”not supported” to give users better feedback than “check” and “em dash”.

    • Adrian SANDU

      Hi Ted,

      Thank you for the suggestions. They are most welcome.

  • gskema

    Exactly what I will be working on pretty soon. There are pretty good but I think I’ll stick with responsive tables by Zurb. The first column stays in place whiel you scroll the data cols.

  • Martin

    You can replace

    td,
    th {
    width: 33.333333333333%;
    }

    with

    flex: 1 auto;

    and it will now support any number of columns without having to specify a width. Makes the code more modular.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Front-end, once a week, for free.