Sticky Headers for Table Views

I have been trying for a while to find a simple solution for sticky headers on a table in Knack. While this solution may not suit everyone, it seems to have made my user base very happy and it seems to work on all modern browsers I've tested it on. Chrome, Firefox, IE, Edge and Safari. Seems to work on PC and Mac.

This solution was largely taken from Stack Overflow https://stackoverflow.com/questions/673153/html-table-with-fixed-headers/20381437 with a few extra tweaks for my liking.

JS

/* Sticky Headers  */

$(document).on("knack-view-render.view_xxxx", function (event, view, data) {

  var v = document.getElementById("view_xxxx");
  var w = v.getElementsByClassName("kn-table-wrapper")[0];
  var t = v.getElementsByTagName("table")[0];


  $(w).attr('id', 'view_wrap_xxxx');

  $(t).attr('id', 'view_table_xxxx');

  document.getElementById("view_wrap_xxxx").addEventListener("scroll", function(){

    var translate = "translate(0,"+this.scrollTop+"px)";
    var myElements = this.querySelectorAll("th");

    for (var i = 0; i < myElements.length; i++) {

      myElements[i].style.transform=translate;
    }
  });
});

 

CSS

#view_wrap_xxxx {
  overflow: auto !important;
  height: 800px !important;
}

 

 

That's it.

 

Enjoy

Hi Jordan,

Contact me via email and we'll see if we can work this out.

Tony

tony at omegaim.com.au

Hi Jordan,

I've revisited the code and taken out all the unnecessary bits and made it multi device. Try this:

 

Javascript:

/* Sticky Headers for Table 1 */

$(document).on(“knack-view-render.view_2614”, function (event, view, data) {

freezeColumn(view.key, 2, 1, 1); // Parameters are (a) view; (b) columns to freeze; (c) rows to freeze; (d) Unique table ID of the scene

});

/* Sticky Headers for Table 2 */

$(document).on(“knack-view-render.view_5780”, function (event, view, data) {

freezeColumn(view.key, 2, 1, 2); // Parameters are (a) view; (b) columns to freeze; (c) rows to freeze; (d) Unique table ID of the scene

});

/* Function to freeze headers and columns */
function freezeColumn(view, cols, headers, tableNumber) {

var v = document.getElementById(view);
var wrapEl = v.getElementsByClassName("kn-table-wrapper")[0];
var tableEl = v.getElementsByTagName("table")[0];

$(wrapEl).attr('id', 'view_wrap_' + tableNumber);
$(tableEl).attr('id', 'view_table_' + tableNumber);

for (var i = 0, row; row = tableEl.rows[i]; i++) {

	//iterate through rows

	if (i &lt; headers) {
		for (var j = 0, headerCell; headerCell = row.cells[j]; j++) {
			if (j &lt; cols) {
				row.cells[j].classList.add("freeze-corner");
			} else {
				row.cells[j].classList.add("freeze-header");
			}
		}
	} else {
		for (var j = 0; j &lt; cols; j++) {

			tableEl.rows[i].cells[j].classList.add("freeze-column");

			if (i % 2 == 0) {
				row.cells[j].style.backgroundColor = "#f8f8f8";
			} else {
				row.cells[j].style.backgroundColor = "white";
			}
		}
	}
}

var leftHeaders = tableEl.querySelectorAll('.freeze-column');
var topHeaders = tableEl.querySelectorAll('.freeze-header');
var tableCorner = tableEl.querySelectorAll('.freeze-corner');

wrapEl.addEventListener('scroll', function () {

	var x = wrapEl.scrollLeft;
	var y = wrapEl.scrollTop;

	tableCorner.forEach(function (tableCorner) {
		multiTrans(tableCorner, x, y);
	});

	leftHeaders.forEach(function (leftHeader) {
		multiTrans(leftHeader, x, 0);
	});

	topHeaders.forEach(function (topHeader) {
		multiTrans(topHeader, 0, y);
	});
});

}

/* Function to make the transform translate work for multiple devices */
function multiTrans(el, x, y) {

$(el).css({
	"transform": translate(x, y),
	"msTransform": translate(x, y),
	"webkitTransform": translate(x, y),
	"MozTransform": translate(x, y),
	"OTransform": translate(x, y)
});

}

/* Function to make the transform translate a little clearer */
function translate(x, y) {

return 'translate(' + x + 'px, ' + y + 'px)';

}

 

CSS:

#view_table_1 .corner {
  position: relative;
  top: 0px;
  background-color: #e5e5e5;
  z-index: 9 !important;
}

#view_table_1 .header-cell {
position: relative;
top: 0px;
background-color: #e5e5e5;
z-index: 8;
}

#view_table_1 .freeze-c1 {
position: relative;
top: 0px;
z-index: 7;
}

#view_wrap_1 {
overflow: scroll;
max-height: 800px;
}

#view_table_1 {
border-collapse: separate;
border-spacing: 1px;
}

#view_table_2 .corner {
position: relative;
top: 0px;
background-color: #e5e5e5;
z-index: 9 !important;
}

#view_table_2 .header-cell {
position: relative;
top: 0px;
background-color: #e5e5e5;
z-index: 8;
}

#view_table_2 .freeze-c1 {
position: relative;
top: 0px;
z-index: 7;
}

#view_wrap_2 {
overflow: scroll;
max-height: 800px;
}

#view_table_2 {
border-collapse: separate;
border-spacing: 1px;
}

 

Thanks

Hi Jordan,

Have a read of this:

https://stackoverflow.com/questions/29864790/why-on-safari-the-transform-translate-doesnt-work-correctly

You'll need to add extra lines of code in the js (at each transform line of code) to get safari to work smoothly.

thanks,

Hi Jordan,

Try adding this function to freeze columns and headers:

 

Here is my scenario. I have two tables on the one page (scene) and I want to freeze columns and headers on both tables. Because I am adding and id to each table and table-wrapper, I need to be careful to ensure I assign unique names to them while at the same time, keeping the code as low maintenance as possible.

So I have built a function based on pieces of code from Stack Overflow https://stackoverflow.com/questions/673153/html-table-with-fixed-headers/20381437 and https://shuheikagawa.com/blog/2016/01/11/freeze-panes-with-css-and-a-bit-of-javascript/ and https://gist.github.com/chrisjhoughton/7890303 that does what I want.

 

Javascript:

 

/* Sticky Headers and Columns for Table 1 of a scene */

$(document).on(“knack-view-render.view_xxxx”, function (event, view, data) {

freezeColumn(view.key, 2, 1, 1); // Parameters are (a) view; (b) columns to freeze; (c) rows to freeze; (d) Unique table ID of the scene

});

/* Sticky Headers and Columns for Table 2 of the same scene */

$(document).on(“knack-view-render.view_yyyy”, function (event, view, data) {

freezeColumn(view.key, 2, 1, 2); // Parameters are (a) view; (b) columns to freeze; (c) rows to freeze; (d) Unique table ID of the scene

});

/* Function to freeze headers and columns */
function freezeColumn(view, cols, headers, tableNumber) {

if (!document.getElementById(view)) {

	console.log(view + " does not exist so take no further action.");

} else {

	var v = document.getElementById(view);
	var w = v.getElementsByClassName("kn-table-wrapper")[0];
	var t = v.getElementsByTagName("table")[0];

	$(w).attr('id', 'view_wrap_' + tableNumber);

	$(t).attr('id', 'view_table_' + tableNumber);

	var tableId = document.getElementById("view_table_" + tableNumber);
	var divWrap = document.getElementById("view_wrap_" + tableNumber);

	waitForEl(tableId, function () {

		var maxRows = tableId.rows.length;

		for (var i = 0, row; row = tableId.rows[i]; i++) {

			//iterate through rows

			if (i &lt; headers) {

				for (var j = 0, headerCell; headerCell = row.cells[j]; j++) {
					if (j &lt; cols) {
						row.cells[j].classList.add("corner");
					} else {
						row.cells[j].classList.add("header-cell");
					}
				}

			} else {

				for (var j = 0; j &lt; cols; j++) {

					tableId.rows[i].cells[j].classList.add("freeze-c1");

					if (i % 2 == 0) {
						row.cells[j].style.backgroundColor = "#f8f8f8";
					} else {
						row.cells[j].style.backgroundColor = "white";
					}
				}
			}
		}

		var leftHeaders = tableId.querySelectorAll('.freeze-c1');
		var topHeaders = tableId.querySelectorAll('.header-cell');
		var tableCorner = tableId.querySelectorAll('.corner');

		var topLeft = document.createElement('div');
		var computed = window.getComputedStyle(topHeaders[0]);

		divWrap.appendChild(topLeft);

		topLeft.classList.add('top-left');
		topLeft.style.width = computed.width;
		topLeft.style.height = computed.height;

		divWrap.addEventListener('scroll', function () {

			var x = divWrap.scrollLeft;
			var y = divWrap.scrollTop;

			tableCorner.forEach(function (tableCorner) {
				tableCorner.style.transform = translate(x, y);
			});

			leftHeaders.forEach(function (leftHeader) {
				leftHeader.style.transform = translate(x, 0);
			});
			topHeaders.forEach(function (topHeader, i) {
				if (i === 0) {
					topHeader.style.transform = translate(x, y);
				} else {
					topHeader.style.transform = translate(0, y);
				}
			});
			topLeft.style.transform = translate(x, y);
		});
	});
}

}

/* Function to make the transform translate a little clearer */
function translate(x, y) {

return 'translate(' + x + 'px, ' + y + 'px)';

}

/* Function to wait for the element to appear in the DOM */

var waitForEl = function (selector, callback) {
if ($(selector).length) {
callback();
} else {
setTimeout(function () {
waitForEl(selector, callback);
}, 100);
}
};





CSS:

.top-left { position: fixed; } #view_table_1 .corner { position: relative; top: 0px; background-color: #e5e5e5; z-index: 9 !important; } #view_table_1 .header-cell { position: relative; top: 0px; background-color: #e5e5e5; z-index: 8; } #view_table_1 .freeze-c1 { position: relative; top: 0px; z-index: 7; } #view_wrap_1 { overflow: scroll; max-height: 800px; } #view_table_1 { border-collapse: separate; border-spacing: 1px; } #view_table_2 .corner { position: relative; top: 0px; background-color: #e5e5e5; z-index: 9 !important; } #view_table_2 .header-cell { position: relative; top: 0px; background-color: #e5e5e5; z-index: 8; } #view_table_2 .freeze-c1 { position: relative; top: 0px; z-index: 7; } #view_wrap_2 { overflow: scroll; max-height: 800px; } #view_table_2 { border-collapse: separate; border-spacing: 1px; }


Hi Jr.s,

The above code does work in Full width mode. However, the easiest way to implement sticky header is with CSS only. Like this:

 

CSS:

/* Sticky Headers without javascript */

th {
  position: sticky;
  top: 0px;
}

.kn-table {
  border-collapse: separate !important;
  border-spacing: 1px !important;
}


/* Sticky Headers - Repeat  for each table you want a sticky header   */

#view_xxx .kn-table-wrapper {
max-height: 700px;
}

#view_yyy .kn-table-wrapper {
max-height: 700px;
}

 

Thanks 

Thank you Tony

Dear Mr Tony,

Wish you are well.

Since a week ago , while I was panning the table , the freezed column seems to be sent to back and no longer be visible while panning.

Do you know what’s the cause of this problem?

Thank you so much !

Have a blessed day.

Something change Knack side recently as my sticky headers stopped working @Johnny_Parsons_86 provided the below updates CSS, change view_XXXX

/==================STICKY HEADERS========================/
thead {
position:sticky;
top:0px;
z-index:20;
}
/* Sticky Headers - Repeat for each table you want a Sticky Header /
#view_XXXX.kn-table-wrapper {
max-height: 700px;
}
/
======END========STICKY HEADERS======END================*/

2 Likes

Hi @CarlHolmes

When I use this code my headers are no longer crossing over with data in the table but the header now moves up and down in the table with the scroll.

This doesn’t happen if i remove “z-index:20’” from the code - but does break the sticky headers again.

Does the code work for you?

There must be some conflicting code. I don’t get this, works fine for me :man_shrugging:

1 Like

Let’s hope that the work Knack are doing on Tables at the moment is going to include sticky headers and first ‘n’ columns!!

1 Like