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================*/

3 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

@CarlHolmes

Thanks for the code which works perfectly. Just a couple of things to note because I did have issues initially when I tried to use it (copying it).

  1. It is missing the /* and */ in title

/* ==================STICKY HEADERS======================== */

  1. needs to have a space before .kn-table-wrapper

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

Thanks @ZiadQ

I just checked the text file in my Dropbox and those two items were addressed. There is a link in the YouTube video description. :+1:

3 Likes

Hi Carl

I tried those mods but what I end up with isa a table header row that disappears or migrates down the page.
Here what my table display looks like at start


And when scrolling with the code added.

Is something else in the CSS of which I have very limited knowledge causing grief ???

Ray

Looks like you have the app set to “Max Width” in the settings. The code only seems to work if you have it set to “Full Width”. There could be some other conflict with the CSS given the customisation you have.

Yes I played with max width/full width and that seems to be the case.
Unfortunately the look with full width is desirable for our setup.
Will have to put in a bug report I guess.
Find it bit strange that such a standard feature of “tables” across modern computing even has an issue

Hi Ray

We’re like you and prefer Max Width and had the same result using Carl’s code.

We’re currently using the following code which is functional but not visually perfect during scrolling until someone more knowledgeable posts a better solution.

thead {
position:relative;
top:-1px;
z-index:5;
}

Dean

Like minds I found this same reference to the issue being a javascript issue in the stackoverflow forums and with that same solution.

What I WAS also experiencing also is that if a table field is coloured based on a value (i.e GOOD = green background or BAD - red background) the table field title disappears completely.
Same thing applies of the table field is a URL embedded reference.

The solution your offer does do the job of overlaying the info in the table on partially displayed rows and retains the heading title.

I’ve opted to offset by -10px. That places it back where it should be although the small price to pay is a thin gap at the top when first displaying the table. Can live with that for a much more desirable and workable outcome.

This is the way it should be - and thence (I THINK) no need for sticky table headers code.

Added your snippet with my mod as the very first entry on the CSS for my system (as I have no clue where it should be order wise :slight_smile: ).

Works across the board even on modal popups !

Cheers for following through mate - much appreciated

PS Have you changed the color of the background of the header row at all ?
Not managed to find where to put the paramter to get it done successfully.
Followed all the notes metods I could find

Hi Ray

It looks like it requires a combination of CSS and Javascript to achieve a true sticky header rather than the band-aid solution we’re both now using. Unfortunately, I don’t have time at present to solve this minor irritation.

See https://www.w3schools.com/howto/howto_js_sticky_header.asp

Dean

1 Like