Freezing Columns In HTML Tables

Where I sadly have to go beyond CSS and reach for JavaScript to achieve frozen columns in HTML tables.
Published on Monday, 3 November 2025

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: sticky to make the row/column sticky.
  • We set top: 0 for the sticky row, and left: 0 for 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:

  1. Use fixed widths for the columns, so we can calculate the offsets in advance.
  2. 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.



Blog Logo

Hi! thanks for dropping in to pick my brain

I write on this blog for my own benefit. I write because I like to, and because it helps me process topics. It is also my own little home on the web and a place for me to experiment.

Since you're here though, I'd love to hear your thoughts on what you've read here, so please leave a comment below. Also, if you like what you read and want to give a small tip, fell free to:

Buy Me A Coffee