Simple jQuery Fixed Column Table Solution

2 min read

I had problems using jQuery plugins to handle building a fixed column table on a AJAX-y page so I made my own very simple function. Feel free to take and use it as you want, but don't forget the styling!

The Problem

This past week at work our developers built a cool timeline view of medications someone was taking. Each month in the year is a column and each medication is a row, which can lead to long and tall tables. To combat this I wanted the user to be able to scroll left, right, up, and down on the table while still being able to see the medication column (the first column). Because our application is used on both desktop and on iPads, I knew the solution had to always viewed, not just at a certain viewport.

I found several jQuery plugins that did exactly this, but they all had math to redraw the table and do other things that weren't necessary. These plugins also were not working correctly, because the table for this page comes after the document and window has already loaded. To combat this, I set out to write my own simple and concise function that did exactly what I needed and nothing more.

The Solution

1// Pinned table JS - jQuery/JS
2function pinTable() {
3 var original = $("table.responsive").wrap("<div class='table-wrapper'/>");
4 var copy = original.clone();
5 copy.find('td:not(:first-child),th:not(:first-child)').remove();
6 copy.removeClass('responsive');
7 original.closest(".table-wrapper").append(copy);
8 copy.wrap("<div class='pinned'/>");
9 original.wrap("<div class='scrollable'/>");
10 copy.find('tr').each(function(i) {
11 $(this).height(original.find('tr:eq('+i+')').height());
12 });
15// Pinned table styles - SCSS
16table.responsive {
17 margin-bottom: 0;
18 td, th {
19 position: relative;
20 white-space: nowrap;
21 overflow: hidden;
22 }
24table.responsive th:first-child, table.responsive td:first-child, table.responsive.pinned td {
25 display: none;
27.pinned {
28 position: absolute;
29 left: 0;
30 top: 0;
31 background: #fff;
32 width: 20%;
33 overflow: hidden;
34 overflow-x: scroll;
35 border-right: 1px solid #ccc;
36 border-left: 1px solid #ccc;
37 table {
38 border-right: none;
39 border-left: none;
40 width: 100%;
41 }
42 table th, table td {
43 white-space: nowrap;
44 }
45 td:last-child {
46 border-bottom: 0;
47 }
49div.table-wrapper {
50 position: relative;
51 margin-bottom: 20px;
52 overflow: hidden;
53 border-right: 1px solid #ccc;
54 div.scrollable {
55 margin-left: 20%;
56 overflow: scroll;
57 overflow-y: hidden;
58 }

The Nitty Gritty

Let's break down the Javascript function line-by-line for those reading that may want to learn something and/or understand how it works.

var original = $("table.responsive").wrap("<div class='table-wrapper'/>");

I chose to add a class of responsive to the table that would be used in the pinTable() function. This makes the function reusable and doesn't require any other setup like passing in the selector string as a parameter (though you can do it if you want). I also wrap this table.responsive in a div.table-wrapper which has special styles applied to it. I also store this in an original variable.

var copy = original.clone();

.clone() is a special jQuery manipulation that creates a deep copy of the elements matched. Deep copying means that it will copy and create the matched elements and all descendants of that element(s). I store this cloned table in a copy variable.


I then find all descendants of the copied table that are a td and th cell, but are not the first child and remove them. This leaves me with the first column of the table.


I remove the responsive class from the cloned table.


Then I find the closest element with .table-wrapper and append the cloned table to it.

copy.wrap("<div class='pinned'/>");

I wrap the cloned table in a div with a special class for the pinned column.

original.wrap("<div class='scrollable'/>");

Then wrap the original full table in a div with a special class with scrollable CSS.

1copy.find('tr').each(function(i) {
2 $(this).height(original.find('tr:eq(' + i + ')').height())

Lastly, I find each tr and set the height of that tr to the height of the original table's tr that has the same index (using the eq method).


I broke down the function line-by-line to help you understand what exactly is happening and how easy it is to build something like this.