← All posts

Responsive and SEO Friendly Data Tables

· CSS, HTML, Responsive

First appeared on mobifreaks.com · Updated

This was one of the most popular posts on Mobifreaks. The idea was a data table that reflowed into stacked cards on phones, with each value labelled. The responsive part still holds up. The "SEO friendly" part, building the table out of divs, does not, so I have rewritten that half.

What I did in 2011

We had all moved away from table-based layouts, and I believed tables were bad for search engines. So I rebuilt the table with divs and gave them the browser's own table display values:

.table      { display: table; }
.table-head { display: table-header-group; }
.row        { display: table-row; }
.column     { display: table-cell; padding: 10px 20px; border-bottom: 1px solid #eeeeee; }
<div class="table">
  <div class="table-head">
    <div class="column">Employee No</div>
    <div class="column">Real Name</div>
    <div class="column">Profession</div>
  </div>
  <div class="row">
    <div class="column" data-label="Employee No">2311</div>
    <div class="column" data-label="Real Name">Ipsum dolor</div>
    <div class="column" data-label="Profession">Web designer</div>
  </div>
</div>

Then, under 640px, a media query turned every row into a block, hid the header and printed each cell's data-label in front of its value with content: attr(data-label). That trick is the part worth keeping.

What I got wrong

Tables were never bad for SEO. They were bad for layout. For tabular data, a real <table> is exactly right: search engines understand it, and screen readers can announce the column header for every cell, which a pile of divs can't do.

The same idea today

Keep the real table, add the labels, and only change the display on small screens.

<table class="data">
  <caption>Employees</caption>
  <thead>
    <tr><th scope="col">Employee No</th><th scope="col">Real Name</th><th scope="col">Profession</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-label="Employee No">2311</td>
      <td data-label="Real Name">Ipsum dolor</td>
      <td data-label="Profession">Web designer</td>
    </tr>
  </tbody>
</table>
.data { border-collapse: collapse; width: 100%; }
.data th,
.data td { padding: 0.625rem 1.25rem; border-bottom: 1px solid #eeeeee; text-align: left; }

@media (width <= 40rem) {
  .data thead {
    /* visually hidden, still read by screen readers */
    position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
  }
  .data,
  .data tbody,
  .data tr,
  .data td { display: block; }
  .data tr { margin-block: 1.25rem; border: 1px solid #eeeeee; }
  .data td::before {
    content: attr(data-label);
    display: block;
    font-weight: bold;
    font-size: 0.75rem;
  }
}
Live demo. Drag the bottom-right corner to resize it. Open in a new tab
  • @media (width <= 40rem) is the newer range syntax for max-width. All current browsers support it.
  • The header is hidden visually rather than with display: none, the same trick I used in 2011 with top: -1000em, just tidier.
  • One catch: some browsers drop a table's meaning for screen readers once you change its display. If you need to be sure, add role="table", role="row", role="columnheader" and role="cell" to the elements.
  • If the columns are mostly short, a simpler option is to let the table scroll sideways inside a wrapper with overflow-x: auto.

A follow-up post, Responsive Data Tables with jQuery, did the 2011 conversion automatically.