William Bain

CSS Trivia: Rotated table headers in 2019

Recently I built an HTML report to aggregate the results of some automated jobs. The table headings being much longer than the table’s contents, I wanted to set the headings at a 45° angle. When I Googled for how to do this with CSS, the answers I got were all years old and involved some complications that seem to have become unnecessary. Since I didn’t find a current guide, I wanted to record what I settled on.

The top Google result for “css rotate table header 45 degrees” is a 2014 CSS Tricks article which rather breezily describes a variation on an earlier technique from another blog post.1 The trail of posts traces back to one from 2009, a very different era in web development. At the time, the best technique was to rotate the header with its top left corner staying fixed, and then to apply what the CSS Tricks article calls “some math stuff using tan() and cos()” to figure out what translation needed to be applied in order for the bottom right of the rotated header to meet the top right-hand corner of the cell below.2

The key to the updated approach is that instead of rotating from the top left and then correcting the horizontal and vertical positioning, we can keep the bottom left point of the header fixed by setting transform-origin. Then we only need to offset the horizontal position of the text by the width of the table column, a constant we’ll already have in our CSS.

2009
2019

This neatly takes care of the core problem of positioning the headings, although we’ll still hit some awkwardness related to having transformed elements in the CSS layout. The example below demonstrates the technique:

Year
Splines reticulated
Ipsums loremmed
Observations on the theory and practice of landscape gardening
2016 120 1,900 25
2017 3,002 14,000 16
2018 20,124 980 48
<style>
  .scrollable {
    overflow: auto;
  }

  .rotated-header th {
    height: 240px;
    vertical-align: bottom;
    text-align: left;
    line-height: 1;
  }

  .rotated-header-container {
    width: 75px;
  }

  .rotated-header-content {
    width: 300px;
    transform-origin: bottom left;
    transform: translateX(75px) rotate(-45deg);
  }

  .rotated-header td:not(:first-child) {
    text-align: right;
  }
</style>

<figure class="scrollable">
  <table class="rotated-header">
    <thead>
      <tr>
        <th>Year</th>
        <th>
          <div class="rotated-header-container">
            <div class="rotated-header-content">Splines reticulated</div>
          </div>
        </th>
        <th>
          <div class="rotated-header-container">
            <div class="rotated-header-content">Ipsums loremmed</div>
          </div>
        </th>
        <th>
          <div class="rotated-header-container">
            <div class="rotated-header-content">
              Observations on the theory and practice of landscape gardening
            </div>
          </div>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>2016</td> <td>120</td> <td>1,900</td> <td>25</td>
      </tr>
      <tr>
        <td>2017</td> <td>3,002</td> <td>14,000</td> <td>16</td>
      </tr>
      <tr>
        <td>2018</td> <td>20,124</td> <td>980</td> <td>48</td>
      </tr>
    </tbody>
  </table>
</figure>

There are a few points worth noting:

  • In this demo I’m using two divs inside each th: a wrapper to set the width of the header cell and an inner div for the rotated text. In theory I think it should be possible to fix the dimensions of the cell by styling the th itself, but I haven’t dug into the specifics of CSS table layout rules to figure out how to make it work.

  • I rely on a vertical-align rule to push the header text to the bottom of the fixed-size th elements. Something similar can also be done with flexbox.

  • I’ve set the hardcoded height of the header heuristically; in principle one could do “math stuff” to get the minimum height that includes the rotated header but in this demo the height of the rotated header is determined by the height of multiple lines of text; getting the necessary parameters dynamically is probably possible on the client side, but requires knowing exactly how the header text will be laid out. This is tricky, particularly if the font size or leading change responsively based on the window size.

  • Part of the rotated headers extend outside the area of the table itself. In this demo I’ve wrapped the table in a scrollable container and made it left-aligned. While the scrollable area of the container includes the protruding headers, if the table is centered within the scrollable container it will be centered according to its own size, excluding the headers. This can push the headers outside the area which is visible without scrolling.

Bonus: Fallbacks

Support for CSS transforms is pretty robust, but what happens if a user agent lacks it? In my case, the problem child was the Outlook web client, which allows embedded CSS in emails, but strips transform styles for what I imagine are security reasons. Without my making special provisions, this resulted in the unrotated row headers overlapping and becoming unreadable.

Fortunately, there’s a simple fix: wrap all of the rules related to header rotation in a @supports(transform: ...) query. Clients not supporting the transform will render an unwieldy but basically readable table. With support for @supports being a bit spottier than support for the CSS transforms themselves, this probably results in some extra user agents rendering the fallback, but for me that’s an acceptable cost for what is basically a progressive enhancement.

  1. CSS Tricks is an excellent resource; the post I stumbled on is the result of having a deep back catalog. As I was preparing this post I used posts from the same author to refresh my memory on how to animate SVG. 

  2. The older posts also skew the shape of the header in order to have its horizontal lines stay parallel with the horizontal lines of the body rows. This is less relevant now that tables with external borders are largely out of style.