← All posts

Responsive Data Tables with jQuery

· JavaScript, Responsive

First appeared on mobifreaks.com · Updated

In October 2011 I wrote Responsive and SEO Friendly Data Tables, which got a lot of attention. Readers then asked for a plugin that would convert a normal HTML table into that responsive version automatically, so I got my hands dirty and wrote my first jQuery plugin.

What the 2012 plugin did

It read the header cells, then replaced every element of the table with a div, adding each column's header text as a data-label on its cells:

// jQuery 1.7
var head_col_count = $('thead td').size();
for (i = 0; i <= head_col_count; i++) {
  var head_col_label = $('thead td:nth-child(' + i + ')').text();
  $('tr td:nth-child(' + i + ')').replaceWith(function () {
    return $('<div class="column" data-label="' + head_col_label + '">').append($(this).contents());
  });
}
$('table').replaceWith(function () { return $('<div class="table">').append($(this).contents()); });
$('thead').replaceWith(function () { return $('<div class="table-head">').append($(this).contents()); });
$('tr').replaceWith(function () { return $('<div class="row">').append($(this).contents()); });

Thanks to useyourfred, whose responsive tables post inspired it.

Why it needs replacing

  • .size() was removed in jQuery 3. It would throw an error today; .length replaced it.
  • Replacing the table with divs threw away its meaning for screen readers and search engines. As I explain in the updated tables post, a real table is the right element.
  • The header text went into the attribute unescaped, so a quote in a heading would break the markup.
  • The loop started at 0, but :nth-child starts at 1, so the first pass did nothing.

The same idea today

Keep the table and only add the labels. The CSS from the tables post does the rest. No jQuery needed:

for (const table of document.querySelectorAll('table.data')) {
  const headers = [...table.querySelectorAll('thead th')].map((th) => th.textContent.trim());
  for (const row of table.querySelectorAll('tbody tr')) {
    [...row.cells].forEach((cell, i) => {
      if (headers[i] && !cell.dataset.label) cell.dataset.label = headers[i];
    });
  }
}
Live demo. Drag the bottom-right corner to resize it. Open in a new tab
  • row.cells gives you the row's cells in order, so there is no need for :nth-child.
  • dataset.label sets data-label safely, whatever characters the heading contains.
  • Cells that already have a label keep it.
  • If your tables are generated on the server or at build time, add the data-label attributes there and skip the JavaScript entirely.