HTML Table – Freeze Row and Column with CSS

Having a freeze pane effect on HTML Table. Freeze/Fixed/Frozen the Columns and Rows in HTML Table by using CSS.

This work was inspired by an article written by Manish Malviya, and I would like to thank for his sharing of freezing the table rows columns at https://blogs.perficient.com/2021/01/18/freezing-row-and-column-in-html-table-using-css/

Video Walkthrough Guide

Examples

Example 1: As shown in Youtube Tutorial Video

Example 2: Fixed width & height

Example 3: Responsive width & height (adjusted by using viewport)

Let’s Start

Take a simple HTML Table as example:

<div class="div1">
    <table>
        <tr>
            <th>Column 1</th>
            <th>Column 2</th>
            <th>Column 3</th>
            <th>Column 4</th>
            <th>Column 5</th>
            <th>Column 6</th>
        </tr>
        <tr>
            <td>Row Data 1</td>
            <td>Row Data 2</td>
            <td>Row Data 3</td>
            <td>Row Data 4</td>
            <td>Row Data 5</td>
            <td>Row Data 6</td>
        </tr>
        <tr>
            <td>Row Data 1</td>
            <td>Row Data 2</td>
            <td>Row Data 3</td>
            <td>Row Data 4</td>
            <td>Row Data 5</td>
            <td>Row Data 6</td>
        </tr>

        <tr>
            <td>Row Data 1</td>
            <td>Row Data 2</td>
            <td>Row Data 3</td>
            <td>Row Data 4</td>
            <td>Row Data 5</td>
            <td>Row Data 6</td>
        </tr>
    </table>
</div>

Here is the CSS that does the magic:

.div1 {
    width: 600px;
    height: 400px;
    overflow: scroll;
    border: 1px solid #777777;
}

.div1 table {
    border-spacing: 0;
}

.div1 th {
    border-left: none;
    border-right: 1px solid #bbbbbb;
    padding: 5px;
    width: 80px;
    min-width: 80px;
    position: sticky;
    top: 0;
    background: #727272;
    color: #e0e0e0;
    font-weight: normal;
}

.div1 td {
    border-left: none;
    border-right: 1px solid #bbbbbb;
    border-bottom: 1px solid #bbbbbb;
    padding: 5px;
    width: 80px;
    min-width: 80px;
}

.div1 th:nth-child(1),
.div1 td:nth-child(1) {
    position: sticky;
    left: 0;
    width: 150px;
    min-width: 150px;
}

.div1 th:nth-child(2),
.div1 td:nth-child(2) {
    position: sticky;
    /* 1st cell left/right padding + 1st cell width + 1st cell left/right border width */
    /* 0 + 5 + 150 + 5 + 1 */
    left: 161px;
    width: 50px;
    min-width: 50px;
}

.div1 td:nth-child(1),
.div1 td:nth-child(2) {
    background: #ffebb5;
}

.div1 th:nth-child(1),
.div1 th:nth-child(2) {
    z-index: 2;
}

Explanation

First of all, a DIV tag is used to contain the TABLE, providing a fixed width and height to turn a very long and wide HTML table into scrollable table.

.div1 {
    width: 600px;
    height: 400px;
    overflow: scroll;
    border: 1px solid #777777;
}

The attribute of “overflow: scroll” will make the table scrollable.

For building a responsive table, the CSS function “CALC” can be used to auto calculate the width, such as:

.div1 {
    height: calc(100vh - 250px);
    width: calc(100vw - 100px);
    overflow: scroll;
    border: 1px solid #777777;
}

Please note that the space is required in between the CALC values.

For example, this is wrong:

height: calc(100vh-250px);

and this is correct:

height: calc(100vh - 250px);

vh” or “vw” are “Viewport” units.

  • 100vh = 100% visible height, it’s something like window size, it refers to the visible area.
  • 100vw = 100% visible width.

Styling the TABLE

.div1 table {
    border-spacing: 0;
}
  • border-spacing: 0, eliminates the empty distance between cells

Note that the attribute of “border-collapse: collapse” cannot be used in this case. This is because the border line will behave incorrectly with “position: sticky” which will be discussed later below.

Styling the “TH” (table header)

.div1 th {
    border-left: none;
    border-right: 1px solid #bbbbbb;
    padding: 5px;
    width: 80px;
    min-width: 80px;
    position: sticky;
    top: 0;
    background: #727272;
    color: #e0e0e0;
    font-weight: normal;
}
  • position: sticky, this will make the TH cells always stay at top position
  • top: 0, this tells TH cells to always stay at position 0 (zero) measured from top
  • background, without background color, the bottom TD cells will “crash” into TH cells, making them overlap with each other
  • width, min-width: this is used to fix the column width, without these attributes, the cell columns will be deformed and compressed

This will freeze the “Header”.

Freezing the 1st Column

.div1 th:nth-child(1),
.div1 td:nth-child(1) {
    position: sticky;
    left: 0;
    width: 150px;
    min-width: 150px;
}
  • nth-child(1) means the first element in each “TR” block. Refers to 1st column.
  • left: 0 tells the cells to “freeze” at position zero from left.

Freezing the 2nd Column

.div1 th:nth-child(2),
.div1 td:nth-child(2) {
    position: sticky;
    /* 1st cell left/right padding + 1st cell width + 1st cell left/right border width */
    /* 0 + 5 + 150 + 5 + 1 */
    left: 161px;
    width: 50px;
    min-width: 50px;
}

Calculation of next cell position is: Border Width + Padding + Cell Width

In this case, 0 left border width + 5px left padding + 150px (1st cell width) + 5px right padding) + 1px right border width = 161px.

Hence, left: 161px

Next, when the table is scrolled to the right, the non-sticky cells will be crashing into and overlapping with the 1st and 2nd frozen cells.

Provide a background color for 1st and 2nd frozen cells to fix the overlapping issue:

.div_maintb td:nth-child(1),
.div_maintb td:nth-child(2) {
    background: #ffebb5;
}

Now, the first two frozen “TH” and “TD” are both “sticky”. Since “TD” is created after “TH”, when the table is being scrolled down, the “TD” will stay on top and cover up the “TH”, making “TH” hide under “TD”.

Thus, we can set the CSS value of “z-index” of “TH” to override it’s layer to be brought to front/top, so that “TD” will now go under/behind “TH“.

By default, all elements has default value of “z-index=0“.

.div1 th:nth-child(1),
.div1 th:nth-child(2) {
    z-index: 2;
}

Done 🙂