How to freeze columns in HTML tables
Generally, this sounds like something that should be possible with just CSS, and it is - to some extent.
Below, I've defined a little wrapper class, just to be able to set height and width limits on a table, to be able to see the scrollbars. And then, I've defined utility classes for making the table freezable, and to mark that I want first row and first column to be frozen.
<div class="freezable-wrapper">
    <table class="freezable fr-1 fc-1">
              ...
    </table>
</div>
The CSS looks like this:
.freezable-wrapper {
  /* Constrain the width and height for the sake of testing */
  max-height: 200px;
  max-width: 600px;
  overflow-y: auto;
  margin-bottom: 1rem;
}
.freezable {
  max-height: 50px;
  overflow-y: auto;
  
}
.freezable.fr-1 {
  /* Sticky row */
  tr:nth-child(1) th {
    background-color: red;
    position: sticky;
    top: 0;
  }
}
.freezable.fc-1 { 
  /* Sticky column */
  tr > td:nth-child(1), th:nth-child(1) {
    background-color: green;
    position: sticky;
    left: 0;
  }
  tr > th:nth-child(1) {
    z-index: 9000;
    background-color: blue;
  }
}
It is nothing fancy. There are only a few things to look out for:
- We use 
position: stickyto make the row/column sticky. - We set 
top: 0for the sticky row, andleft: 0for the sticky column. - We set background colors to avoid seeing through to the content and to make it easier to see what is going on in the example.
 
This works great when all we need is to freeze the first column or row; but it immediately falls apart when we want to freeze more than one. If we extend the approach to freeze the first two columns, it would look like this:
.freezable.fc-2 {
  tr > td:nth-child(1), th:nth-child(1) {
    background-color: green;
    position: sticky;
    left: 0;
  }
  tr > th:nth-child(1) {
    z-index: 9000;
    background-color: blue;
  }
  
  tr > td:nth-child(2), th:nth-child(2) {
    background-color: green;
    position: sticky;
    left: 0;                                       /* PROBLEM! Won't work without fixed widths. Can't use 0, but need to offset by column 1's width :( */
  }
  tr > th:nth-child(2) {
    z-index: 9000;
    background-color: blue;
  }
}
As noted in the comment there, the problem is that both the first and the second column are set to left: 0, so they will overlap. To make this work, we need to offset the second column by the width of the first column. This means that we need to know the width of the first column in advance. There is no way for this to be calculated dynamically in CSS, so we're left with two options:
- Use fixed widths for the columns, so we can calculate the offsets in advance.
 - Use JavaScript to calculate the widths dynamically and set the offsets accordingly.
 
A full, working sample of the project so far is available here
See the Pen Frozen columns and rows by Ruben Nielsen (@eldamir) on CodePen.
The JavaScript solution
If you've read some of my other posts, you may know that I am not a big fan of where the JavaScript ecosystem has been headed for years; trying to JavaScript all the things. I believe we should stick to HTML/CSS when that is sufficient, and only reach for JavaScript when absolutely necessary.
This is one of those cases. I shared my solution above with the community on Mastodon, and got a friendly response:
This approach is awesome, since it wraps the behaviour in a Web Component, which is web-standard and can be used anywhere without dependencies and without worrying about whatever JS framework is in season.
You can follow the progress of this approach here.
See the Pen Multiple sticky columns by Ruben Nielsen (@eldamir) on CodePen.
The whole idea is to use JavaScript for measuring the left offset for any column that needs to be frozen. At the time of writing, there are a few things that are not working yet:
- It doesn't take into account that cells might dynamically change size (e.g. window resize or content changes)
 - It doesn't take into account colspan/rowspan
 - It doesn't take into account changes to the colgroup/col elements in the table
 
I will try to address these issues over time, but for now, this is a workable solution for freezing columns in HTML tables. I will return with an update on this post if I solve the remaining issues.