BookStack Hack: Dynamic collapsible left sidebar

Some background

For our active learning kits, such as our Shark Skull Active Learning Kit, we’ve created open-source guides that lead users through the various kit activities. Our first method for creating these guides was to write them in Google Docs, export the guides as PDFs, and upload these PDFs to our website. We also made the Google Doc publicly available so that anyone could create a copy and edit it to customize the guides (that’s the open-source part).

However, this method had a few challenges: making changes was time consuming (update Google doc, export PDF, upload PDF, update link), edits by other users were not available to other users since they were made to their own copy, and there was no way to add dynamic content (e.g., videos, collapsible elements, etc.) directly into the guides because these wouldn’t transfer over to a PDF.

For these reasons, we’ve decided to migrate our existing and future activity guides to BookStack. BookStack is a self-hosted, open-source platform created by Dan Brown for organizing hierarchical content. BookStack has some similarities to a Wiki (e.g., it’s free and open-source, multiple users can create and edit content with little to no coding skill needed, content is searchable). But we decided that BookStack would be a better fit than a Wiki for hosting our activity guides because of its built-in hierarchical system.

All content is added to the platform either as a page, chapter, book, or shelf (listed in increasing hierarchical position), which makes it easy for us to organize our activity guide content in a way that is intuitive for users. For example, each of our kits will have a “shelf,” with a “book” for each of the kit activities, and all the content for each activity will be organized into various “chapters” and “pages.” Another thing that we love about BookStack is the ability to use code to customize the functionality and layout of the platform and the community of users and developers actively improving the platform (yay for open-source!).

The hack and how it works

In keeping with this open-source culture of BookStack, I wanted to share some custom code (i.e., hack) that I have developed that allows users to dynamically show and hide the left sidebar, expanding the width of the main page content for full page viewing on midsize screens. This has been a feature request for BookStack (e.g., Dynamically show or hide sidebars #4461 and Make pages bigger #1757) but hasn’t been implemented as a core/included functionality.

You can see this hack in action on this this BookStack page from our own hosted BookStack instance (click on the button/text “Collapse left sidebar” at the top left of the screen) or in the images below. I’ve tested this custom code with the most recent version of BookStack, v25.11.4 (the most recent as of the original publication date of this post, November 28, 2025). But of course —standard disclosure— this custom code is not officially supported and may cause instability, introduce issues or conflict with future updates.

Example BookStack page with left sidebar collapsed, leaving a small SVG icon (upper left margin) to re-expand the sidebar.

For this hack I used the standard method for BookStack, which is to add custom Javascript and CSS code to the “Custom HTML Head Content” in the Customization page of the Settings. The code adds a “Collapse left sidebar” button to all BookStack webpages (for pages, chapters, books, and shelves) that is only visible when the main webpage content is in the “two-column layout” (i.e., when there is a single sidebar to the left occupying 30% of the window width and a main content block occupying the remaining 70% of the window to the right).

Example of a BookStack page in a two-column state. The custom code has added the “Collapse left sidebar” button to the top of the left sidebar.

Clicking on this button hides the left sidebar content while preserving the small SVG icon so that the user has a button to click to re-open the sidebar (I used the same SVG icon as the one used to toggle open/closed the right sidebar when editing pages on BookStack). Clicking on the button also creates a cookie that will register the most recent state of the left sidebar so that if you navigate to the next page in a book (or any other page for that matter), the left sidebar stays closed (or open).

A collapsible sidebar is not needed when viewing BookStack pages on a narrow screen (i.e., a tablet or mobile device) since BookStack automatically rearranges the window contents into single main content and “Info” columns (interchangeable by clicking on the corresponding header button) when the window width is less than 1001 pixels.

Example of a BookStack page viewed on a narrow screen with header buttons allowing the user to switch between viewing the page info or the main content. No collapse sidebar button is needed when the window is this width since there is no sidebar.

Likewise, a collapsible sidebar is not needed when viewing BookStack pages on a wide screen (in my opinion) because there is enough room to accommodate both a left and right sidebar while leaving plenty of space for the main content. At window widths greater than 1400 pixels, BookStack automatically rearranges the window contents into a three-column layout with a left sidebar, main content, and right sidebar.

Example of a BookStack page viewed on a wide screen with a three-column layout. No collapse sidebar button is needed because there is enough space to accommodate both sidebars and a wide main content block.

When the window is either too narrow or too wide to necessitate a collapsible left sidebar, the custom code reopens the sidebar (restores the default formatting) so that the sidebar is visible in the info column (on narrow screens) or to the left (on wide screens). The custom code also hides the “Collapse left sidebar” button. If the user changes the window width to the middle (i.e., two-column format) size, the left sidebar will re-collapse (if it was previously collapsed). That is, the collapse status will update automatically with dynamic window width changes.

One issue with this code is that the collapsing of the sidebar occurs after all of the document objects have been loaded. So, if you have the left sidebar collapsed and navigate to another page, the sidebar will “flash” briefly and then collapse rather than being collapsed from the very start of the page loading. This would be great to fix in the future but the code in its current state is functional and I think the momentary flash of the sidebar is OK.

The code

Below is the custom code to be copied into the “Custom HTML Head Content” in the Customization page of the Settings.

Here’s the CSS code:

<style>
/* Styling for collapsible left sidebar */
@media screen and (min-width: 1401px), screen and (max-width: 1000px) {
	/* Hide left sidebar collapse button when screen is too narrow or too wide */
	.collapse-left-side-bar-button {
		display: none;
	}
	.tri-layout-sides-content {
		margin-top: 0px;
	}
}
@media screen and (min-width: 1001px) and (max-width: 1400px) {
	.tri-layout-sides-content {
		top: -15px; /* Adjust from default of 0px for collapse button */
		margin-top: -20px; /* Decreases gap between left sidebar collapse button and text */
	}
}
.collapse-left-side-bar-button-div {
}
.collapse-left-side-bar-button-div-collapsed {
	text-align: center;
}
.collapse-left-side-bar-button {
	position: sticky;
	outline: none;
	margin: 50px 0px 10px -5px;
	color: #777; /* rgb(117, 117, 117) Matches grayed out text in sidebar wo hover */
}
.collapse-left-side-bar-button-collapsed {
	margin-left: auto;
	margin-right: auto;
	width: 100%;
}
.collapse-left-side-bar-button:hover {
	color: #444; /* rgb(68, 68, 68) Matches darker text in sidebar w hover */
	cursor: pointer;
}
.collapse-left-side-bar-svg {
	float: left;
	top: 3px;
}
.collapse-left-side-bar-svg-collapsed {
	transform: rotate(180deg);
	margin-inline-end: 0px;
	float: none;
	left: -1px;
}
.collapse-left-side-bar-button-text {
	float: left;
}
.collapse-left-side-bar-button-text-collapsed {
	display: none;
}
.tri-layout-container-collapsed {
	grid-template-columns: 0.07fr 3fr !important;
	grid-column-gap: 0px;
	margin-left: 0px;
}
.tri-layout-sides-content-collapsed {
	margin-top: -35px;
	top: -35px;
}
.tri-layout-right-collapsed, .tri-layout-left-collapsed {
	display: none;
}
</style>

And here’s the Javascript code:

<!-- Make left sidebar collapsible -->	
<script type="text/javascript">

// Declare global variables
let previousInnerWidth = window.innerWidth;
sidebarOpenedOnResize = false;

function getCookie(name) {
	const value = `; ${document.cookie}`;
	const parts = value.split(`; ${name}=`);
	if (parts.length === 2) return parts.pop().split(';').shift();
}

// Function that collapses and opens left sidebar 
function toggleLeftSideBar() {

	// Toggle collapsed class for sidebar container (changes width)
	const tri_layout_container_matches = document.getElementsByClassName('tri-layout-container');
	tri_layout_container_matches[0].classList.toggle('tri-layout-container-collapsed');

	// Toggle collapsed class for sidebar content (shows/hides text)
	const tri_layout_sides_content_matches = document.getElementsByClassName('tri-layout-sides-content');
	tri_layout_sides_content_matches[0].classList.toggle('tri-layout-sides-content-collapsed');

	// Toggle collapsed class for sidebar content (shows/hides text)
	const tri_layout_right_matches = document.getElementsByClassName('tri-layout-right');
	tri_layout_right_matches[0].classList.toggle('tri-layout-right-collapsed');
	const tri_layout_left_matches = document.getElementsByClassName('tri-layout-left');
	tri_layout_left_matches[0].classList.toggle('tri-layout-left-collapsed');

	// Toggle collapsed class for button text (shows/hides text)
	const button_text = document.getElementById('collapse-left-side-bar-button-text');
	button_text.classList.toggle('collapse-left-side-bar-button-text-collapsed');
	
	// Toggle collapsed class for button div
	const button_div = document.getElementById('collapse-left-side-bar-button-div');
	button_div.classList.toggle('collapse-left-side-bar-button-div-collapsed');
	
	// Toggle true/false value for aria-expanded property of button
	const button = document.getElementById('collapse-left-side-bar-button');
	if(!button.ariaExpanded){
		button.title = 'Open left sidebar';
	}else{
		button.title = 'Collapse left sidebar';
	}
	// Change value
	button.ariaExpanded = button.ariaExpanded !== 'true';
	button.classList.toggle('collapse-left-side-bar-button-collapsed');

	// Make session cookie - whether left sidebar is expanded by click (not by window resize)
	if(!sidebarOpenedOnResize){
		document.cookie = "bookstack_leftsidebar_expanded=" + button.ariaExpanded + "; path=/"; // No expires attribute makes it a session cookie
	}

	// Toggle collapsed class for SVG element (rotates 0/180 deg)
	const button_svg = document.getElementById('collapse-left-side-bar-svg');
	button_svg.classList.toggle('collapse-left-side-bar-svg-collapsed');
}

// 
window.addEventListener('resize', function() {

	// Get window dimensions
	const currentInnerWidth = window.innerWidth;
	const button = document.getElementById('collapse-left-side-bar-button');

	if(previousInnerWidth >= 1001 && previousInnerWidth <= 1400){
		// Window was previously inside thresholds

		if(currentInnerWidth < 1001 || currentInnerWidth > 1400){
			// Window is now outside thresholds

			if(button.ariaExpanded == 'false'){
				sidebarOpenedOnResize = true;
				toggleLeftSideBar()
				//console.log('Left sidebar opened due to window width change (' + previousInnerWidth + ' --> ' + currentInnerWidth + ')');
			}
		}
	}else{

		// Window was previously outside thresholds

		if(currentInnerWidth >= 1001 && currentInnerWidth <= 1400){
			// Window is now inside thresholds

			if(sidebarOpenedOnResize && button.ariaExpanded == 'true'){
				sidebarOpenedOnResize = false;
				toggleLeftSideBar()
				//console.log('Left sidebar collapsed due to window width change (' + previousInnerWidth + ' --> ' + currentInnerWidth + ')');
			}
		}
	}

	// Set new previous window width
	previousInnerWidth = currentInnerWidth;
});

document.addEventListener("DOMContentLoaded", function() {

	// Add button to sidebar
	const sidebar_contents_matches = document.getElementsByClassName('tri-layout-sides-content');
	var sidebar_contents_div = sidebar_contents_matches[0];
	if (sidebar_contents_div) { // If an element was found

		// Create new div for button
		const newButtonDiv = document.createElement('div');
		newButtonDiv.id = 'collapse-left-side-bar-button-div';
		newButtonDiv.classList.add('collapse-left-side-bar-button-div');

		// Create new button
		const newButton = document.createElement('button');
		newButton.id = 'collapse-left-side-bar-button';
		newButton.type = 'button';
		newButton.refs = 'editor-toolbox@toggle';
		newButton.title = 'Collapse left sidebar';
		newButton.setAttribute('aria-expanded', 'true'); 
		newButton.classList.add('toolbox-toggle');
		newButton.classList.add('collapse-left-side-bar-button');
		newButton.addEventListener('click', function() {
			toggleLeftSideBar();
		});
		
		// Create SVG container for circle and triangle
		const svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		svgContainer.id = 'collapse-left-side-bar-svg';
		svgContainer.classList.add('svg-icon');
		svgContainer.classList.add('collapse-left-side-bar-svg');
		svgContainer.role = 'presentation';
		svgContainer.setAttribute('viewBox', '0 0 24 24'); 
		svgContainer.setAttribute('data-icon', 'caret-left-circle'); 

		// Create background circle and append to SVG container
		const path1 = document.createElementNS("http://www.w3.org/2000/svg", 'path');
		path1.setAttribute('d', 'M0 0h24v24H0z');
		path1.setAttribute('fill', 'none');
		svgContainer.appendChild(path1);

		// Create triangle and append to SVG container
		const path2 = document.createElementNS("http://www.w3.org/2000/svg", 'path');
		path2.setAttribute('d', 'M12 22c5.52 0 10-4.48 10-10S17.52 2 12 2 2 6.48 2 12s4.48 10 10 10m2-14.5v9L8 12z');
		svgContainer.appendChild(path2);

		// Append the SVG container to the button
		newButton.appendChild(svgContainer);

		// Create div for button text
		const newButtonText = document.createElement('div');
		newButtonText.id = 'collapse-left-side-bar-button-text';
		newButtonText.innerHTML = 'Collapse left sidebar';
		newButtonText.classList.add('collapse-left-side-bar-button-text');
		newButton.appendChild(newButtonText);

		newButtonDiv.appendChild(newButton);
		sidebar_contents_div.prepend(newButtonDiv);
	}

	const leftSidebarExpanded = getCookie('bookstack_leftsidebar_expanded');

	if (typeof leftSidebarExpanded !== 'undefined') {
		// Cookie for state of left sidebar collapse is defined

		if(leftSidebarExpanded == 'false'){
			// Left sidebar should be collapsed
			
			if(window.innerWidth >= 1001 && window.innerWidth <= 1400){
				// Window width is inside thresholds, collapse left sidebar
				toggleLeftSideBar();
			}else{
				// Window width is outside thresholds, will collapse left sidebar 
				// if window is resized to within thresholds
				sidebarOpenedOnResize = true;
			}
		}
	}
});
</script>

Here’s some additional CSS code that I use to further expand the main content block. It’s not necessary for the collapsible sidebar functionality but if you’re interested in maximizing the main content width, it’s probably something you’ll want to add.

<style>
/* Expand width of page content */
/* Allows middle (page) content to expand to fill width of screen on extra wide screens */
@media screen and (min-width: 1400px) {
	.tri-layout-middle-contents {
		max-width: 1700px;
	}
}
.page-content { 
	max-width: 1600px; /* Allows page-content within middle content space to expand to 
						fill, leaving 50 px on each side as margin */
}
/* Between 1001px and 1400px (inclusive), the page will have a two-column layout with a single 
	sidebar on the left and page content on the right */
@media screen and (min-width: 1001px) and (max-width: 1400px) {
	.tri-layout-container {
		padding-right: 0px; /* This value is 24px by default for two-column layout. 
			Setting to 0 px gives page content a bit more width while keeping 
			symmetrical margins */
	}
}
</style>

If you have any suggestions for improvement or if you find any bugs, leave a comment below!

Let us know what you think!

Your email address will not be published. Required fields are marked *