A jQuery plugin for a fixed table of contents

John Avis by | October 12, 2018 | Web Development

Some long web pages have a handy feature where a table of contents are shown beside the content which remains fixed.
A jQuery plugin for a fixed table of contents

Some long web pages have a handy feature where a table of contents are shown beside the content which remains fixed.

A good example of this was the Bootstrap 3.x documentation pages. Once you scroll down past a certain point the table of contents position changes to fixed so it is always visible.

There is a problem with this particular implementation though. If the table of contents list is long and exceeds the height of the browser window, then it is impossible to see the bottom part of the table of contents.

In Bootstrap 4.x (example) they changed the way tables of contents were displayed to avoid this problem. They are now always fixed position and if the content cannot fit in the vertical space they show a secondary scroll bar just for the table of contents.

This works well enough, but I was looking for a more automatic method closer to the way it was done on the Bootstrap 3.x pages for my Bootstrap Customizer website, but with the ability to be able to scroll to see any content that wouldn't fit in the browser window.

This is quite a challenging task as the requirements are a little complex:

1) The TOC (table of contents) displays and scrolls as normal until you scroll down to the end of the TOC. At this point the TOC displays as fixed with the bottom of TOC at the bottom of the browser window.

2) When scrolled down to the end of the page, the TOC should not be displayed over the footer, and the end of the TOC should display at the end of it's container.

3) When you scroll up the TOC should also scroll up. This should only happen up to the point where the top most item in the TOC is at the top of the screen, or has reached it's original natural position.

Based on these requirements, I've been working on a prototype.

(function ($) {
	$.fn.scrollfixed = function (options) {
		var settings = $.extend({
			breakpoint: 0
		}, options);

		var obj = this;
		var isFixed = false;
		var atBottom = false;
		var lastScrollTop = -1;
		var contents_top = obj.offset().top;

		var contentTop = 0;
		var contentTopOriginal = obj.offset().top;

		function handleScroll() {
			var contentHeight = obj.outerHeight(true);
			var containerTop = $('#' + options.container).offset().top;
			var containerHeight = $('#' + options.container).height();
			var containerBottom = containerTop + containerHeight;
			var viewportHeight = $(window).height();
			var scrollTop = $(window).scrollTop();

			var scrollingUp = scrollTop < lastScrollTop;
			var scrollingDown = scrollTop > lastScrollTop;

			var nextIsFixed;
			var nextContentTop;

			if (window.matchMedia('(min-width: ' + options.breakpoint + 'px)').matches && containerHeight !== contentHeight) {

				if (scrollTop > contentTopOriginal) {
					nextIsFixed = true;

					if (contentHeight <= containerHeight && (containerBottom - scrollTop - contentHeight) < 0) {
						if ((containerBottom - scrollTop - contentHeight) < 0) {
							nextContentTop = (containerBottom - scrollTop - contentHeight);
						else {
							nextContentTop = 0;
					else {
						nextContentTop = contentTop;

						if (scrollingDown) {
							if (scrollTop > containerBottom - (contentTop + contentHeight)) {
								nextContentTop += (lastScrollTop - scrollTop);
							else if ((contentTop + contentHeight) > viewportHeight) {
								nextContentTop += (lastScrollTop - scrollTop);

							if (nextContentTop < (viewportHeight - contentHeight)) {
								nextContentTop = (viewportHeight - contentHeight);
						else if (scrollingUp) {
							if (contentTop < 0) {
								nextContentTop -= (scrollTop - lastScrollTop);

							if (nextContentTop > 0) {
								nextContentTop = 0;
				else {
					nextIsFixed = false;
					nextContentTop = 0;
					task = 'fits???';
			else {
				nextIsFixed = false;
				nextContentTop = 0;

			if (nextIsFixed !== isFixed) {
				obj.css("position", (nextIsFixed === true ? "fixed" : ""));
				obj.css("left", (nextIsFixed === true ? obj.offset().left + "px" : ""));

			if (nextContentTop !== contentTop || nextIsFixed !== isFixed) {
				obj.css("top", (nextIsFixed === true ? nextContentTop + "px" : ""));

			isFixed = nextIsFixed;
			contentTop = nextContentTop;

			lastScrollTop = scrollTop;

		$(window).scroll(function () {

			handleScroll._tId = setTimeout(function () {
			}, 10);


		return this;

This is done as a jQuery plugin, and can be called with parameters "breakpoint" (the pixel min-width at which the positional changes should take effect, so you can disable this functionality on small screens if needed) and "container" (the ID of the container object that should be used to calculate minimum and maximum positions - this would usually be something like a Bootstrap column).

$("#myTOC").scrollfixed({ breakpoint: 768, container: 'myTOCContainer' });


This achieves all of the objectives but there's still a few minor issues with this code which I need to improve.

Change History

12-Dec-2020 - Code revised and improved, demo added

Related Posts

Intermittent "Unable to read data from the transport connection: net_io_connectionclosed" errors

by John Avis | May 6, 2020

If you are having intermittent problems sending email in .NET using System.Net.Mail consider switching libraries.

500 Internal Server Error after migrating from IIS 7.5 to IIS 10

by John Avis | November 4, 2019

As support ends for Microsoft Windows Server 2008 I have recently gone through migrating some websites to a new server running Windows Server 2016 and IIS 10 but some of the websites did not work.

tagInput: A simple jQuery plugin for tag entry using Bootstrap 4

by John Avis | October 15, 2019

For a website project I needed a way to enter multiple tags. I just wanted something simple that I could easily modify to suit my own needs, so I wrote my own.


There are no comments yet. Be the first to leave a comment!

Leave a Comment



...random postings about web development and programming, Internet, computers and electronics topics.

I recommend ASPnix for web hosting and Crazy Domains for domain registration.


Get the latest posts delivered to your inbox.