1

I'm trying to implement frozen row and column headers (similar to Excel) in a table within a content wrapper. Due to the required presence of other shifting elements on the screen, the CSS tricks that leverage the position:absolute property doesn't seem to work reliably for me.

The idea is that when a page loads, the table is presented as shown below. The gray area represents the surrounding content that I can't remove. The red, green and purple boxes represent the row and column headers that I want to be frozen, so that they stay in place while the user scrolls or pans the white region.

Frozen headers starting position

As shown, the content very likely will stretch vertically beyond what's initially shown in the browser, so as the user scrolls down ideally the column headers will bump up to the top, illustrated below. However, this is a nice-to-have and not required. I'd be grateful even if the headers would just stay put where they're first loaded on the page.

Frozen headers after vertical scrolling

Also, like Excel, scrolling horizontally should leave the row headers alone while causing the column headers to move with the content.

Frozen headers after horizontal scrolling

Is there any way to reliably achieve this functionality? I've created a JSFiddle that illustrates the scenario and the content wrapper in which I'm working: http://jsfiddle.net/Jr5Zt/3/

<table id="bodyTable">
    <tbody><tr><td id="bodyCell">
        <!-- my custom content -->
    </td></tr></tbody>
</table>
Marty C.
  • 576
  • 2
  • 7
  • 22
  • I've done fixed column headers in pure CSS before, and it's a real real pain. At a high level, wrap the `` contents within `` tags, absolutely position those `` tags, and add enough top padding to the `` to make room for the headers. That may not sound too bad, but when you start dealing with styling the headers, it spins out of control very quickly. The thought of doing something similar with row "headings" at the same time gives me nightmares. If I were you, I'd only pursue this if it were really important and you have a lot of time to debug it. – Stephen Thomas Feb 11 '14 at 21:49
  • There are also some approaches using JavaScript (e.g. clone the table, hide the body of the clone and the header row of the original, etc.) But those approaches have their own problems. – Stephen Thomas Feb 11 '14 at 21:51
  • Does this work? http://stackoverflow.com/questions/20759920/html-table-with-fixed-header-column-and-row-without-js – Marcelo Somers Feb 14 '14 at 16:35
  • Thanks for the suggestion, Marcelo. I think the idea of using divs is the right way to go for implementing floating or "frozen" headers. Using a table is too limiting, although it's certainly convenient. – Marty C. Feb 14 '14 at 19:53

1 Answers1

0

I've not found pure html/css solution that does the following:

  • uses tables semantically
  • fixes both header and column
  • works with variable width columns
  • works with vertical and horizontal scrolling

Here's something I hacked together that does work

http://codepen.io/saiidi/pen/mePdqo

The thing I don't like about it is that the content in the header th elements is duplicated. I think with a little additional hacking this could be fixed.

In the spirit of putting code in the answer, here's the code:

html:

<div class="scrolly">
  <div class="scrollx">
    <table>
      <thead>
        <tr>
          <th class="sticky"><div>1</div></th>
          <th class="sticky"><div>2</div></th>
          <th>header 1<div>header 1</div></th>
          <th>header 2<div>header 2</div></th>
          <th>header 3<div>header 3</div></th>
          <th>header 4<div>header 4</div></th>
          <th>header 5<div>header 5</div></th>
          <th>header 6<div>header 6</div></th>
          <th>header 7<div>header 7</div></th>
          <th>header 8<div>header 8</div></th>
          <th>header 9<div>header 9</div></th>
          <th>header 10<div>header 10</div></th>
          <th>header 11<div>header 11</div></th>
          <th>header 12<div>header 12</div></th>
          <th>header 13<div>header 13</div></th>
          <th>header 14<div>header 14</div></th>
          <th>header 15<div>header 15</div></th>
          <th>header 16<div>header 16</div></th>
          <th>header 17<div>header 17</div></th>
          <th>header 18<div>header 18</div></th>
          <th>header 19<div>header 19</div></th>
          <th>header 20<div>header 20</div></th>
          <th class="fill"></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td class="sticky">hello 1</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 2</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 3</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 4</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 5</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 6</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 7</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
        <tr>
          <td class="sticky">hello 8</td>
          <td class="sticky">hello</td>
          <td>world 1</td>
          <td>world 2</td>
          <td>world 3</td>
          <td>world 4</td>
          <td>world 5</td>
          <td>world 6</td>
          <td>world 7</td>
          <td>world 8</td>
          <td>world 9</td>
          <td>world 10</td>
          <td>world 11</td>
          <td>world 12</td>
          <td>world 13</td>
          <td>world 14</td>
          <td>world 15</td>
          <td>world 16</td>
          <td>world 17</td>
          <td>world 18</td>
          <td>world 19</td>
          <td>world 20</td>
          <td class="fill"></td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

css:

td, th {
  padding: 5px;
  white-space: nowrap;
}

td {
  background: linear-gradient(135deg, white 0%, #a80077 99%, black 100%);
}

th {
  height: 0;
  font-weight: normal;
}

.scrollx {
  max-width: 100%;
  overflow-x: scroll;
}

.scrolly {
  position: relative;
  max-height: 150px;
  overflow-y: scroll;
  margin-bottom: 20px;
}

table {
  border-collapse: collapse;
  min-width: 100%;
}

table td.fill,
table th.fill {
  width: 100%;
  min-width: 0;
  background: white;
  padding: 0;
}

tr {
  position: relative;
}

.sticky {
  text-decoration: underline;
  font-weight: 700;
}

.stuck {
  position: absolute;
}

thead th {
  position: relative;
}
thead th div {
  padding: 5px;
  background: linear-gradient(135deg, black 0%, #a80077 99%, white 100%);
  color: #ffffff;
  position: absolute;
  z-index: 2;
  top: 0;
  left: 0;
  right: 0;
}

js:

$(function() {
  $("#status").text('loaded');


  $("table").each(function(index, table) {
    var firstRow = $($(table).find('tr')[0]);
    var offset = 0;
    var stickies = firstRow.find('.sticky');

    firstRow.children().each(function(index, td) {
      var width = $(td).width();
      $(table).find('tr td:nth-of-type('+(index+1)+')').css({width: width + 'px'});
      $(table).find('tr th:nth-of-type('+(index+1)+')').css({width: width + 'px'});
    });

    stickies.each(function(index, td) {
      var column = $(table).find('tr .sticky:nth-of-type('+(index+1)+')');
      column.css({left: offset+'px'});
      column.addClass('stuck');
      offset += $(td).width() + 10;
    });

    $(table).parent().css({"margin-left": offset+'px'});

    $(table).parent().parent().scroll(function(e) {
      var top = e.currentTarget.scrollTop;
      $(table).find('thead tr th div').css({top:top+'px'});
    });
  });
});

So, what's this doing?

it's using absolute positioning on the fixed (or "sticky") columns to keep the stuck on the left. To support variable widths, it's calculating the width before setting the position to absolute. Then a margin is applied to the left of the table container to compensate for the absolutely positioned columns.

For fixing the header, it's absolutely positioning the nested divs on the th elements, and then adjusting their "top" css property whenever the table is scrolled. Why the duplicated content? It's there so that the column width correctly takes into account the width of the header content.

This is a pretty rough implementation - the jquery code here is meant to be a proof of concept.