A Responsive Design Approach for Complex, Multicolumn Data Tables

Posted by Maggie on 12/29/2011

Topics:

In responsive web design, one of the toughest design problems to solve is how format complex tabular data for display on smaller screens. In this post, we'll explore an experimental approach to rendering a complex table, using progressive enhancement and responsive design methods, that displays comfortably at a wide range of screen sizes, provides quick access to the data, and preserves the table structure so that data can still be compared across columns.

We've been batting around the idea of making tables responsive for awhile.

Our initial attempts to make table data palatable on small screens include showing a thumbnail image that links to the data or a canvas-based chart; others have developed responsive CSS workarounds that display a definition list, using either list or table markup. Which approach to take depends on the type of data. For example, structured data where each row is a unique object or entity—say, business contacts, or favorite Netflix movies—are well-suited to a definition list style on small screens, because header and cell data can be displayed as simple label and value pairs (i.e., Name: Maggie Wachs, Company: Filament Group...). The switch to a chart or other visualization on smaller screens works well for a simple numeric comparison of a single value. And snapping down to a thumbnail image that launched a full table to pan and zoom was an okay fallback in lieu of no data at all.

But we encountered a scenario that didn't quite work with any of the above solutions: a table of complex of financial data with 6-8 related data points, where comparisons and trends among columns are important to see. Visual relationships between headings and cells, and between neighboring columns, are crucial to understanding the data and would be lost on smaller screens if we displayed a list or chart.

We needed a happy medium: a way to keep the basic table structure in place—with headings above, and whole columns that sit side-by-side—and simultaneously display a manageable amount of data at a size that's comfortably readable.

A table for every screen

The approach we devised starts with a full data set, and uses a simple priority-based class designation to display a manageable subset of data columns for common target screen sizes, and also gives the user control to change column visibility easily.

It's probably easier to explain with a concrete example: we'll use a table that lists technology companies and their stock prices and several stock performance metrics. Each row displays data for a single company; columns organize data points by type for comparison. View the demo.

All data columns will display on desktops and tablets in landscape orientation, but only a subset will fit comfortably on anything smaller. So the first order of business is to identify which columns of data are essential to see at all screen widths by default, or optional (shown only when space allows). In our table of tech company stocks, the data is somewhat meaningless without the company name, current stock value, or most recent change, so we'll consider those essential. The trade time, previous close, and open values would be nice to see if the screen can fit those columns, so we'll make them optional. The remaining data—bid, ask, and 1-year target estimate—are less important in relative terms, so they will appear on only the widest screens.


View larger image

In this case, we want users to have the last word regarding which columns to show, so we'll also create a custom menu that lets them choose which columns to display:

The final result is a table that displays a limited set of columns on smaller screens, and provides quick access to data that's hidden because of space constraints (view the demo). To accomplish this, we'll use progressive enhancement to ensure that we're serving a usable experience to all devices. We'll start with well-formed, semantic table markup and very basic CSS, and then if the browser is capable, apply JavaScript and enhanced CSS (including media queries) to conditionally show a larger number of columns as screen space allows.

Markup

A basic table — with consecutive columns and descriptive headings — is an efficient way to display a complex data set; data arranged into columns and rows are easy to scan and compare. So an HTML table is the clear markup choice for our financial data.

We'll start with a well-formed table that contains a thead for the heading row, followed by a tbody for the cells.

<table cellspacing="0" id="tech-companies">
   <thead>
      <tr>
         ...header cells...
      </tr>
   </thead>
   <tbody>
      ...rows of data...
   </tbody>
</table>

As we fill in the content, we'll add descriptive classes to identify the essential and optional content. We'll assign these classes only to the headers; later, we'll write a little JavaScript to map these headers to their respective columns of data.

<thead>
   <tr>
      <th class="essential persist">Company</th>
      <th class="essential">Last Trade</th>
      <th class="optional">Trade Time</th>
      <th class="essential">Change</th>
      <th class="optional">Prev Close</th>
      <th class="optional">Open</th>
      <th>Bid</th>
      <th>Ask</th>
      <th>1y Target Est</th>
   </tr>
   ...
</thead>

Notice that we added a second class to the Company header, persist. Essential columns are present by default at small screen sizes, but we'll still be able to toggle their visibility with the custom menu. Marking the Company column with this class provides a way for us to omit it from the menu, and prevent it from being hidden.

We'll complete the table with rows of data that correspond to the column headers:

<table cellspacing="0" id="tech-companies">
<thead>
   ...
</thead>
<tbody>
   <tr>
      <th>GOOG <span class="co-name">Google Inc.</span></th>
      <td>597.74</td>
      <td>12:12PM</td>
      <td>14.81 (2.54%)</td>
      <td>582.93</td>
      <td>597.95</td>
      <td>597.73 x 100</td>
      <td>597.91 x 300</td>
      <td>731.10</td>
   </tr>
   ...
</tbody>

Later when we apply JavaScript, we'll create a custom menu based on the table's content and append it to the page, immediately above the table. The menu will consist of a container element for a "Display" button and the menu overlay:

<div class="table-menu-wrapper">
   <a href="#" class="table-menu-btn">Display</a>
   <div class="table-menu">
      ...menu content...   
   </div>   
</div>

The menu overlay will contain a list of options, one for each column, where each option has a label and checkbox input for toggling that column's visibility (columns with the persist class will be excluded from the menu):

<div class="table-menu-wrapper">
   <a href="#" class="table-menu-btn">Display</a>
   <div class="table-menu">
      <ul>
         <li>
            <input type="checkbox" name="toggle-cols" id="toggle-col-1" value="co-1">
            <label for="toggle-col-1">Last Trade</label>
         </li>
         <li class="optional">
            <input type="checkbox" name="toggle-cols" id="toggle-col-2" value="co-2">
            <label for="toggle-col-2">Trade Time</label>
         </li>
         ...
      </ul>   
   </div>   
</div>

When the script builds the menu, it will automatically assign name and value attributes and unique IDs to the input elements, and matching for attributes to their labels.

Last but not least, we'll wrap the table in a container element to simplify positioning the menu:

<div class="table-wrapper">
   <table cellspacing="0" id="tech-companies">
      ...
   </table>
</div>

CSS

We'll assume you're already familiar with using CSS3 media queries to render pages responsively. If not, we highly recommend Ethan Marcotte's definitive article, Responsive Web Design, and book by the same title.

Let's start with the table. We want it to fill the available space, so we'll assign a width of 100%:

table {
   width: 100%;
   font-size: 1.2em;
}

And then apply color, padding, and alignment properties to make the data easier to scan:

thead th {
   white-space: nowrap;
   border-bottom: 1px solid #ccc;
   color: #888;
}
th, td {
   padding: .5em 1em;
   text-align: right;
}
th:first-child, 
td:first-child {
   text-align: left;
}
tbody th, td {
   border-bottom: 1px solid #e6e6e6;
}

Next, we'll write the rules that hide and show columns. We've scoped these styles to a class, enhanced, which is assigned to the table via JavaScript. This ensures that column visibility is altered only when JavaScript is available. By default, we'll hide all columns, and show only those marked with the essential class:

.enhanced th,
.enhanced td {
   display: none;
}
.enhanced th.essential, 
.enhanced td.essential {
   display: table-cell;
}

Using CSS3 media queries, we'll show optional columns when the browser is 500px wide or greater, and all columns at 800px or greater:

@media screen and (min-width: 500px) {
   .enhanced th.optional, 
   .enhanced td.optional {
      display: table-cell;
   }
}

@media screen and (min-width: 800px) {
   .enhanced th, 
   .enhanced td {
      display: table-cell;
   }
}

When using this approach, how you prioritize and categorize your data must correspond to the number of screen size breakpoints you plan to support. In our example, we've chosen to have two breakpoints, at 500 and 800 pixels wide, and two levels of importance, essential and optional. If we were to add another break point, say around 400px, we would need to rework our categories to include a third (i.e., primary, secondary, tertiary) so that we can mark each column to be visible at a particular breakpoint.

When the custom menu is inserted into the table's container, it will appear just above the table on the right:

.table-menu-wrapper {
   position: absolute;
   top: -3em;
   right: 0;
}

The menu itself will also be absolutely positioned, and by default will be hidden with the table-menu-hidden class (later, we'll write JavaScript to toggle that class when the "Display" button is clicked):

.table-menu {
   position: absolute;   
   right: 0;
   left: auto;
   background-color: #fff;
   padding: 10px;
   border: 1px solid #ccc;
   font-size: 1.2em;
   width: 12em;
}
.table-menu-hidden {
   left: -999em;
   right: auto;
}

Finally, we'll add relative positioning to the table's container. Later, when we append the menu we can position it without having to calculate location coordinates:

.table-wrapper {
   position: relative;
   margin: 5em 5%;
}

JavaScript

The table we just created is usable on its own; any browser that renders HTML will display it. With a few JavaScript enhancements, we'll be able to view the table at smaller screen sizes without sacrificing the table structure. (The following examples use jQuery.)

First we'll append the enhanced class for scoping styles:

// add class for scoping styles - cells should be hidden only when JS is on
table.addClass("enhanced");

We'll create a container element for the menu, which will come into play a little later in the script:

var container = $('<div class="table-menu table-menu-hidden"><ul /></div>'); 

Then we'll enhance the markup with classes and attributes that allow us to control column visibility. We'll loop through the table headers and assign them unique IDs, then reference those IDs in headers attributes assigned to associated cells. (The headers attribute identifies to which header(s) a cell belongs.) We'll also copy the classes that we assigned to the column headers — essential and optional — and assign them to the associated columns.

$( "thead th" ).each(function(i){
   var th = $(this),
      id = th.attr("id"), 
      classes = th.attr("class");  // essential, optional (or other content identifiers)
         
   // assign an ID to each header, if none is in the markup
   if (!id) {
      id = ( "col-" ) + i;
      th.attr("id", id);
   };      
         
   // loop through each row to assign a "headers" attribute and any classes (essential, optional) to the matching cell
   // the "headers" attribute value = the header's ID
   $( "tbody tr" ).each(function(){
      var cell = $(this).find("th, td").eq(i);                        
      cell.attr("headers", id);
      if (classes) {cell.addClass(classes);};
   });
   ...      

Next, while still looping through the headers, we'll create a menu item for each column, except for those marked with the persist class. Each menu item consists of a checkbox and label with the column header text.

   ...  
   // create the menu hide/show toggles
   if ( !th.is(".persist") ) {
   
      // note that each input's value matches the header's ID; 
      // later we'll use this value to control the visibility of that header and it's associated cells
      var toggle = $('<li><input type="checkbox" name="toggle-cols" id="toggle-col-'+i+'" value="'+id+'" /> <label for="toggle-col-'+i+'">'+th.text()+'</label></li>');
      
      // append each toggle to the container
      container.find("ul").append(toggle);         
      
      ...   

And then we'll bind events to each checkbox for controlling that column's visibility.

      ...  
      
      // assign behavior
      toggle.find("input")
      
         // when the checkbox is toggled
         .change(function(){
            var input = $(this), 
                  val = input.val(),  // this equals the header's ID, i.e. "company"
                  cols = $("#" + val + ", [headers="+ val +"]"); // so we can easily find the matching header (id="company") and cells (headers="company")
         
            if (input.is(":checked")) {cols.show();}
            else {cols.hide();};		
         })
         
         // custom event that sets the checked state for each toggle based on column visibility, which is controlled by @media rules in the CSS
         // called whenever the window is resized or reoriented (mobile)
         .bind("updateCheck", function(){
            if ( th.css("display") ==  "table-cell") {
               $(this).attr("checked", true);
            }
            else {
               $(this).attr("checked", false);
            };
         })
         
         // call the custom event on load
         .trigger("updateCheck");  
         
   }; // end conditional statement ( !th.is(".persist") )
}); // end headers loop   

After closing the headers loop, we'll bind our custom event to the window's resize and orientation change events:

// update the inputs' checked status
$(window).bind("orientationchange resize", function(){container.find("input").trigger("updateCheck");}); 

And, last but not least, append our checkbox menu to the page and bind show/hide menu events:

var menuWrapper = $('<div class="table-menu-wrapper" />'),
   menuBtn = $('<a href="#" class="table-menu-btn">Display</a>');
      
menuBtn.click(function(){
   container.toggleClass("table-menu-hidden");            
   return false;
});
      
menuWrapper.append(menuBtn).append(container);
table.before(menuWrapper);  // append the menu immediately before the table

// assign click-away-to-close event
$(document).click(function(e){								
   if ( !$(e.target).is( container ) && !$(e.target).is( container.find("*") ) ) {container.addClass("table-menu-hidden");}				
}); 

Media query support for IE: Respond.js

Older versions of IE (8 and earlier) don't natively support CSS3 media queries, so we need to use a workaround in those browsers to implement our responsive table. Thanks to our own Scott Jehl, we can use a lightweight polyfill script, respond.js, that enables support for min- and max-width media query properties. The script is open source and available on github.

Keep the conversation going

The pattern discussed here is one possibility for coding a responsive table. We hope to discover more, and will update our RWD-Table-Patterns git repository as we come across additional use cases.

This demo code is open source and available for download. Feel free to put it through its paces. If you use it and see room for improvement, please submit a pull request and we'll review it as soon as possible.

Related plug-ins

We release open source code, including the design pattern here, to encourage collaboration — so we're especially excited to learn of new plugins that extend our work.

MediaTable (in Italian), developed by Marco Pegoraro, is a jQuery plugin that applies responsive behavior to one or more standard tables on a page, and also automates the creation of a menu for showing and hiding columns. It's similar to what we've done here, but in "official" plugin format. The code is open source and available on github. Thanks, Marco!

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site

Comments

Now that’s a very tidy solution, and I particularly like the ability for users to configure their own preferences. Thanks for sharing :)

Comment by Matt Wilcox on 12/30  at  01:10 AM

Great post I love it :) Thx for sharing.

Comment by Stenly Kurinec on 12/30  at  11:46 AM

Very nice approach! Thank you!

Comment by Julian Mengel on 12/31  at  01:38 PM

This. Is. Great.

Thank you for sharing your industry leading, practically-applied knowledge.

Comment by Craig Burnett on 12/31  at  04:39 PM

Nice. I guess we’re revisiting responsive tables again! An elegant solution, thank you.

Comment by Gregory Cox on 12/31  at  07:40 PM

Very nice!

You may wanna use hasClass instead of is() in some places for speed. Perhaps you can use a COLGROUP element to hide/show column instead of changing the style of each cell separately ?

Comment by TeMc on 12/31  at  08:04 PM

This is interesting. Another useful feature might be to create a tabbed view where the hidden columns could be viewed by clicking a tab . See an example in the article http://listui.com/?p=97. You could also added a feature where clicking a row would display all the fields for the row in a vertical alignment.

Comment by Bill Miller on 12/31  at  08:14 PM

If the table in its widest form is really “All data” , then you’ll want to get rid of your column control, as it just adds noise.

Comment by CM Harrington on 12/31  at  08:46 PM

Thanks, its really helpful.

Comment by r0ash on 01/01  at  03:02 AM

Nice article but there is an problem. I have used 24” monitör, if I am not mistaken, the table width &#x10;0 so that when we disable the some column, left section of the table so big.

Comment by Oğuz Çelikdemir on 01/01  at  01:19 PM

Nice article and this link is very useful for css designers and developers http://www.webdesigningtricks.com/?p=112

Comment by muthuselvi on 01/01  at  01:49 PM

Cool. Haven’t thought about that before. Very nice solution for responsive design. Thanks for sharing.

Comment by Rilwis on 01/02  at  08:40 AM

There seems to be a bug in IE7 - as it doesn’t support the table-cell display. Perhaps a targeted display inline and a zoom:1 would help.

Comment by Emma Dobrescu on 01/03  at  01:35 PM

Thanks for the positive feedback, and Happy New Year. :)

@Emma Dobrescu: thanks for catching that.  I just uploaded a fix for IE 7 to the git repo.

@TeMc: we thought of using colgroup/col elements, too, but unfortunately neither effects the display properties of associated columns.  We also didn’t want to assume that all essential or optional columns would be grouped together; it’s possible that they could be distributed throughout the table. 

@CM Harrington and Oğuz Çelikdemir: You both mention issues that should be considered when applying this approach to specific tabular data.  We purposely left the design open-ended to illustrate the concept, but the code is open sourced so feel free to grab it and edit away.

Comment by Maggie (Filament) on 01/03  at  07:52 PM

Very nice idea! I think I might that this thought process and build a slightly different approach, if that’s okay? Think this would be great as a standalone script

Comment by Luke Williams on 01/08  at  09:40 PM

One thing we need to keep in mind when doing things like this is the extra work it places on editors who are under deadlines etc. 

In this case a tool could be created allowing editors to create a table and check columns that are essential without manually putting in all the classes required to make it work. 

I think this points out a hurdle that needs to be approached sooner rather than later with getting editors on board with the reasons for a responsive design and how it affects their work load or additional training they need.

Comment by Scott Cropper on 01/09  at  10:17 PM

Oh man - how I hate, when things are hidden by default, just because I use my smart phone.

It’s really nice, when you offer me a possibility to hide things myself, if I think, they are not important for me, but please: do not decide for me, what I want to see!!!

The unknown user

Comment by Marc Haunschild on 01/11  at  10:33 AM

@Marc Haunschild: we agree with you in principle, especially when functionality is limited on mobile.  (Just this morning I was hamstrung when trying to change a setting in Gmail on my phone; only a few settings are available on mobile.) In this case, though, we think this solution strikes a good compromise:  all content is available at all screen sizes and at all times, with reasonable defaults for screens with very limited space.

@Scott Cropper: interesting point from a manager’s perspective.  That functionality could certainly be layered onto this as part of a CMS.

Comment by Maggie (Filament) on 01/11  at  06:10 PM

Of course you provided here a very nice solution. The setup is really great - sorry for not mentioning this before. As a developer I just would not hide something by default.
I prefer to use responsive techniques (@media) to provide extremely fluid layouts, which anyway keep all elements visible and in well known places (e. g. the search form in the top right corner or the navigation bar on the left side)

Comment by Marc Haunschild on 01/11  at  06:29 PM

Commenting is closed for this post.

Book cover: Designing with Progressive Enhancement

Enjoy our blog? You'll love our book.

For info and ordering: Visit the book site