Introduction
Having a sidebar that allows your visitors to get a general idea of the post's structure, and to skip and freely navigate to any point, is a nice little addition that can make the reading experience more clear and interactive. You can see an example of it in the left sidebar of this website, as long as you're not on mobile.
Creating such a component is really simple and requires a few lines of code, as long as you're already using jQuery on your website (which is very likely). If you aren't, you'll just need to import it.
Importing jQuery
This is easy. Just add the following code between the <head></head>
tags of your blog posts:
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
Remember to replace the 3.5.1
version string with the latest version of jQuery. It won't be a problem for now, but in a few years it might begin to be a little outdated.
Structuring your post's HTML
If you want to have a fully functional sidebar, you'll need to make two divs. The first one is for the sidebar itself; the second one is for your post content. This is because the sidebar's JS code is going to search for titles in the post content's div; then it'll generate links for those paragraphs and add them to the sidebar's div. Remember to give IDs to the headers (if your CMS/SSG isn't doing it already)!
<main>
<aside>
<div id="post__sidebar-left">
<h4>Paragraphs</h4>
<!-- Leave this empty; the script will autofill it -->
</div>
</aside>
<div class="post__content">
<!-- Add your post here. The following lines are just examples -->
<h2 id="hello">Hello</h2>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu dui sit amet diam mollis facilisis sed sed lectus. Mauris vel risus maximus, molestie eros eu, lacinia massa. Ut quis ornare risus. Praesent vestibulum commodo dolor eu lacinia.
<h3 id="subtitle-1">Subtitle 1</h3>
Sed faucibus, purus quis laoreet commodo, ante tellus auctor felis, faucibus tincidunt sapien nisi eu leo.
<h3 id="subtitle-2">Subtitle 2</h3>
Sed faucibus, purus quis laoreet commodo, ante tellus auctor felis, faucibus tincidunt sapien nisi eu leo.
<h2 id="another-title">Another title</h2>
Sed vulputate turpis massa, eget hendrerit nunc ullamcorper sed. Phasellus porttitor aliquet faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque gravida diam id erat interdum, eu bibendum massa interdum. Maecenas cursus nulla quis enim mollis accumsan. Aenean id blandit libero.
</div>
</main>
That's pretty much it for the HTML part. Again, pretty easy!
Making the sidebar with CSS
We wouldn't have called this a sidebar if we didn't intend to make it stick to a side. This part really depends on how your pages are structured, but for this example I'll do it in an extremely simple way: flexbox. Note that this is just so that I can give you a working example, but your configuration might vary wildly and you might prefer to do this in some other way. Anyway, here's the code:
main {
display: flex;
}
Another thing you might want to do is give your currently viewed paragraph a different style - for example, by making it bold, or by coloring it a different way. You can do it, for example, like this:
#post__sidebar-left a.current {
color: red;
}
We are going to apply the current
class via JS later on.
Also, since this is a sidebar, you probably want it to be sticky. Here's a way to do it:
#post__sidebar-left {
position: sticky;
top: 0;
}
Yep, that's it in this case. Tada!
Connecting everything with magic: writing the JS script
This is the hardest part, but fear not: I'll explain the different bits as we go on.
Initializing needed variables
First of all, we're gonna need two arrays to store the paragraphs' headers and their respective links:
let headers;
let sidebarLinks;
Generating the links
Then, when the whole page has loaded, we want to check for every header in the post content's div, and store it in the array we just created:
$(document).ready(function () {
headers = $(".post__content h2, .post__content h3").toArray();
const sidebar = $("#post__sidebar-left");
for (let i = 0; i < headers.length; i++) {
const currentHeader = headers[i];
const headerText = $(currentHeader).text();
const headerId = $(currentHeader).attr("id");
const headerType = $(currentHeader).prop("nodeName").toLowerCase();
const newLine = "<" + headerType + "> <a href=\"#" + headerId + "\">" + headerText + "</a> </" + headerType + ">";
sidebar.append(newLine);
}
sidebarLinks = $("#post__sidebar-left a").toArray();
<...>
});
Let me explain this line by line.
$(document).ready(function () {
<...>
}
This means that we want to wait for the whole document (page) to be loaded before running the script. Also, we only want to run it once (again, when the document has loaded) because the content isn't going to suddenly change, and thus we don't need to check multiple times.
headers = $(".post__content h2, .post__content h3").toArray();
This stores every <h2>
and <h3>
tag found in the post__content
div into the headers
array we just created.
const sidebar = $("#post__sidebar-left");
This is because we need a way to edit the sidebar's content (by adding the paragraphs' links to it). So, we are simply picking the sidebar's div by its unique id (post__sidebar-left
).
for (let i = 0; i < headers.length; i++) {
<...>
}
Now, we want to loop through the headers
array, so that we can process every header, one at a time.
const currentHeader = headers[i];
This is to load the current header we are referring to, inside of the loop.
const headerText = $(currentHeader).text();
const headerId = $(currentHeader).attr("id");
const headerType = $(currentHeader).prop("nodeName").toLowerCase();
This is to load the different properties of the said header: its text ("Title Number One"); its ID ("title-number-one") and its type (h2
, h3
, ...).
const newLine = "<" + headerType + "> <a href=\"#" + headerId + "\">" + headerText + "</a> </" + headerType + ">";
We have to store all this data because we are gonna use it to generate the link in the sidebar. The newLine
is a simple HTML string we are generating at the moment. This string is what we'll be adding to the sidebar's div. For example:
<h2> <a href="header-link"> Header Name </a> </h2>
Now, we want to add this string we just generated to the sidebar:
sidebar.append(newLine);
Voilà! That's it to generate a dynamic sidebar. However, we also want to highlight the currently viewed paragraph.
Highlighting the current paragraph
To do so, we need to run another, last, instruction in the $(document).ready(function(){ }
method:
sidebarLinks = $("#mind-post_sidebar-left a").toArray();
This is so that every link we just generated in the loop will get stored in the sidebarLinks
array we defined at the beginning. Then, we need a new method that checks our reading position whenever we scroll:
$(document).ready(function () {
<...>
$(window).scroll(function () {
let bottomScroll = $(window).scrollTop() + ($(window).height() / 2);
let found = false;
for (let i = headers.length - 1; i >= 0; i--) {
const currentHeader = headers[i];
const currentSidebarLink = sidebarLinks[i];
$(currentSidebarLink).removeClass("current");
const headerVPPosition = $(currentHeader).offset().top;
if (!found && bottomScroll > headerVPPosition) {
$(currentSidebarLink).addClass("current");
found = true;
}
}
});
});
Again, let me explain how and why this works.
$(window).scroll(function () {
<...>
}
This method, that goes inside of the other one ($(document).ready(function () { }
), is called every time we scroll the page. We need this because, obviously, you'll be scrolling the page as you read, and eventually you'll switch to a new paragraph.
let bottomScroll = $(window).scrollTop() + ($(window).height() / 2);
This is to know how may pixels you have scrolled so far. I'm dividing the window height by 2 because this way we'll be looking at the middle of the window instead of the top (I believe most people keep the text they're reading near the middle of the screen, rather than the upmost side).
let found = false;
This is a simple boolean that allows us to know if we have found the div we're looking for, so that we can stop. This will be clearer as you go on.
for (let i = headers.length - 1; i >= 0; i--) {
<...>
}
Again, we are looping through all headers - however, we are doing this from the bottom side rather than from the beginning. This is because we want to stop at the lowest one you're looking at.
const currentHeader = headers[i];
const currentSidebarLink = sidebarLinks[i];
These are two variables we need: the current header we are analyzing in the loop, and its link.
$(currentSidebarLink).removeClass("current");
This is to remove the current
class - we only want the currently viewed paragraph title to have it. We'll remove it from everything and eventually reapply it to the correct div. This is not an issue because if a div is already without that class, it will simply do nothing.
const headerVPPosition = $(currentHeader).offset().top;
This is to know the header's position in pixels from the top, so that we can check if we are effectively looking at it (by comparing it to our current scrolling position).
if (!found && bottomScroll > headerVPPosition) {
$(currentSidebarLink).addClass("current");
found = true;
}
This is the final step! We are checking if we had already found a paragraph we are looking at. If we haven't, then we need to check if the current one is actually the correct one. To check if, we are comparing the pixel coordinates of our screen (bottomScroll
) and the current paragraph (headerVPPosition
).
If this is the correct one, then set found
to true
and add the current
class to the header.
Implementing the script
Now, the last thing you need to do is to save the script we just created, and include it in your HTML file, like this (let's suppose you saved the script in the /scripts/
folder under the name of sidebar.js
):
<script src="/scripts/sidebar.js"></script>
Conclusion
Working Example
Finally, we are done! I hope this wasn't too painful, and that I was clear and exhaustive enough to make you understand everything. Now, to reward you for the super good job you did, here's a working CodePen that you can copy-paste wherever you want!
See the Pen zYKWZvQ by Lorenzo Dellacà (@mind-overflow) on CodePen.