Create a sidebar bullet navigation to smoothly snap to sections

Navigation is a fundamental requirement in many applications, whether it's a web or mobile app. Businesses tend to put in a lot of effort and time to be unique or to stand out from the competition and this extends to their landing pages. A bullet-based sidebar navigation is one of the few quirky methods to do so.

Understanding the effect

The primary component of this effect is the sidebar that is fixed to either left or right side of the viewport, preferably towards the center to attract the most of a user's attention. This component when interacted with must allow users to navigate the page. It is also important to make the jump, as smooth as possible, for which we will use the concept of smooth-scrolling.

demonstration of the effect

Setting up the HTML

For the sake of this tutorial, we will have 3 sections that will take up the full height of the viewport, each will of them will have an id attribute that will help us with navigating between sections and we will add in a few headings in the them, but you can replace the headings with what you want.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sidebar navigation</title>
</head>
<body>
<main>
<section id="first">
<h1>Hello</h1>
</section>
<section id="second">
<h1>How are you?</h1>
</section>
<section id="third">
<h1>Goodbye!</h1>
</section>
</main>
</body>
</html>

Next, we will set up the sidebar, and we will use the semantic HTML tag aside that will contain an unordered list that will contain an anchor tag in each list item. To make things simple, we will use a bunch of small circles, but you can quite literally put anything inside the a tags, and you can style it however you want!

<!DOCTYPE html>
<html lang="en">
<!-- Collapsed -->
<body>
<main>
<aside>
<ul>
<li>
<a class="indicator active" href="#first"></a>
</li>
<li>
<a class="indicator" href="#second"></a>
</li>
<li>
<a class="indicator" href="#third"></a>
</li>
</ul>
</aside>
<!-- Collapsed -->
</main>
</body>
</html>

Styling things up!

The styles will bring our markup and logic together nicely. Firstly, we enable smooth scroll using the scroll-behavior property.

html {
scroll-behavior: smooth;
}
body {
margin: 0;
}

Now we style the aside element. We have to make sure that the sidebar is accessible to the user no matter which section they are in, especially if the scroll bar has been disabled. position: fixed combined with top: 50% can achieve a certain degree of this but we need to use transform to make sure that the aside is 50% from the top of the viewport.

The indicator class will turn the ordinary anchor tag into a white circle with a height and width of 15px. Because the active class increases the scale of these white dots, we make sure that the unordered list is always aligned properly using flexbox.

aside {
position: fixed;
top: 50%;
transform: translate(-50%, -50%);
}
aside ul {
display: flex;
flex-direction: column;
align-items: center;
list-style-type: none;
margin: 0;
}
aside ul li .indicator {
cursor: pointer;
margin-top: 1rem;
display: block;
height: 15px;
width: 15px;
border-radius: 50%;
background-color: white;
}
aside ul li .active {
transform: scale(1.7);
transition: transform 200ms ease-in-out;
}

Next, we make each section take the height of the viewport and give each odd and even section a different background-color. Lots of potentials to do some creative styling here!

section {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
section:nth-child(odd) {
background-color: #ffcc33;
}
section:nth-child(even) {
background-color: #ffb347;
}
section h1 {
margin: 0;
font-family: sans-serif;
font-size: 4rem;
color: white;
}

Remember the active class from before? The stylistic function for it was to help users to identify which of the anchor tags is mapped to the current section. Now we have to make sure that style is applied to the correct anchor tag. To do this get a list of all the anchor tags with the indicator class, then iterate the list each time applying a click event listener.

This listener will first call resetIndicators to remove the active class from all the anchor tags with the indicator class and then safely add the active class.

const indicators = document.querySelectorAll('a.indicator');
const resetCurrentActiveIndicator = () => {
const activeIndicator = document.querySelector(".active");
activeIndicator.classList.remove("active");
};
indicators.forEach((indicator) => {
indicator.addEventListener('click', function () {
resetCurrentActiveIndicator();
this.classList.add('active');
});
});

Activating the indicators while scrolling

If you just tested the code as it is right now, you might’ve notice the awkwardness felt when the you scrolled using the scrollbar and the indicators didn’t get activated. To do this, we can make use of the IntersectionObserver API. The IntersectionObserver class accepts 2 arguments. The first is a callback, and the second is an options object which can be used to configure the behaviour of the observer. The callback will be called whenever the conditions described in the options object are met. For the sake of this tutorial, the only property of the options object that matters is threshold.

The threshold property can be set to a value or an array, it describes at what percentage of the observed element’s visibility in the viewport should the callback function be fire. The onSectionLeavesViewport function creates a IntersectionObserver instance that takes a function that contains the logic that will activate the respective indicator. This particular function receives as an input an array of objects entries, each object contains information on things like whether or not an intersection is happening.

Finally, to make the IntersectionObserver target an element we will call the observe() method on observer which is a reference to the instance of IntersectionObserver.

When about 75% (0.75 threshold) of the observed element (a section) has left the viewport, the callback will be called, and this is when we will reset the active indicator and find the indicator that has to be activated and then activate it. If you want to change when the callback is called, you should alter the threshold value.

const sections = document.querySelectorAll("section");

const onSectionLeavesViewport = (section) => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
resetCurrentActiveIndicator();
const element = entry.target;
const indicator = document.querySelector(`a[href='#${element.id}']`);
indicator.classList.add("active");
return;
}
});
},
{
root: null,
rootMargin: "0px",
threshold: 0.75
}
);
observer.observe(section);
};

sections.forEach(onSectionLeavesViewport);

Adding a fallback for old browsers

Unfortunately, some of the older browsers do not support the scroll-behavior CSS property. As such, if you wish to support older browsers we can make use of the scrollIntoView method. What happens here is we get the element with the href attribute that the current element has (which is an id) and then call the method on it.

This isn't a 100% solid method to support old browsers, scrollIntoViewOptions for example isn't supported in old IE browsers but the only other alternatives are using an external library for smooth scroll, adding a polyfill, or doing the smooth-scroll bit yourself.

/* Collapsed */
indicators.forEach((indicator) => {
indicator.addEventListener('click', function () {
document
.querySelector(this.getAttribute('href'))
.scrollIntoView({ behavior: 'smooth' });
/* Collapsed */
});
});

Take a look at the following codepen and see if everything works as expected! It can be a good reference link too, so you could potentially bookmark it for later.

See the Pen on CodePen.

If you are interested in effects like this and more you should take a look at fullPage. It is a JS library that takes care of a lot of the tricky bits for you (like browser support) and provides a more complete set of features like responsive modes, extensions, CSS conditional classes, callbacks etc, which makes it ideal for apps that go beyond the simple effect that we took a look at.