To create a sticky navbar, you use the position: fixed; CSS property to "stick" your navbar to the viewport, and position: sticky; to make it stick to its parent element.

In this post, you'll learn what all of this means, and how to make a sticky navbar for your own site.

Let's go!

What Is a Sticky Navbar?

But before we proceed, we need to clear one thing up. What exactly do you mean by "sticky"?

You see, in the world of CSS, sticky means something very specific. In fact, there are two settings you use to "stick" an element to a certain part of the screen while the visitor scrolls. These are:

position: sticky;

or ...

position: fixed;

There's a big difference between them:

  • fixed elements stay in the same position on the viewport, regardless of scrolling. So as you scroll down the page, they seem to 'float' over the content.
  • sticky elements are similar - they stay in the same position on the viewport, regardless of scrolling, but only within their parent element. When the viewport reaches the end of a sticky element's parent, it will behave like a relatively positioned element.

In this post, you'll learn how to make a commonly used version of the sticky navbar - one that starts out positioned in your site's header, but not right at the top. Then, as the visitor scrolls past, it "sticks" to the top of the screen, and stays in view as they scroll.

Here's how you do it...

1) Make a Simple Website for the Sticky Navbar

First, let's build a site for the sticky navbar to go in. We'll make a simple one-page site with some jokes from famous comedians.

Each link in our sticky navbar will point to a different section of the site - so we'll use a few <section> elements, with some space for content inside. Here's the structure:

<section id="">
<article>
<h2></h2>
<p></p>
</article>
</section>

And here's the CSS:

@import url('https://fonts.googleapis.com/css2?family=Caveat&family=Poppins:[email protected]&display=swap');

body {
font-family: poppins, sans-serif;
margin: 0px;
}

section {
min-height: 100vh;
}

article {
width: 90%;
max-width: 600px;
padding: 20px;
margin: 0px auto;
}

h2 {
text-align: center;
padding: 40px 0px 0px 0px;
font-size: 3em;
}

p {
font-size: 1.5em;
text-align: center;
}

We can then style each individual section via it's id, so the section for the comedian Bill Hicks could look like this:

section#bill-hicks {
background: rgb(123,211,232);
background: linear-gradient(135deg, rgba(123,211,232,1) 0%, rgba(232,123,165,1) 0%, rgba(255,50,50,1) 100%);
color: #40081a;
}

And after Googling some jokes, and adding some more styles for each section, we get this:

See the Pen on CodePen.

2) Build the Navbar

Now to add the navbar, which we'll place below the #header section in the markup. First the HTML:

<section id="header">
<article>
<h2>Let's read some jokes!</h2>
</article>
</section>
<nav>
<a href="#header">Home</a>
<a href="#tim-vine">Tim Vine</a>
<a href="#bill-hicks">Bill Hicks</a>
<a href="#stewart-francis">Stewart Francis</a>
</nav>

Then some CSS to make it look more presentable, and to position it at the top of the page:

nav {
background-color: #3a24f0;
padding: 10px;
text-align: center;
width: 100%;

/* Pay special attention here! */
position: sticky;
top: 0px;
}

nav a {
color: white;
text-decoration: none;
margin: 0px 10px;
}

nav a:hover {
text-decoration: underline;
}

@media
(prefers-reduced-motion: no-preference)
{
html {
scroll-behavior: smooth;
}
}

Which turns out like this:

See the Pen on CodePen.

How this Makes the Navbar Sticky

So how does this work?

Well, as you may remember, fixed sticks an element to the viewport, and sticky sticks it to its parent element. But here, our navbar is below the header in the markup, rather than being a child of it. So its parent is body!

We gave the element a top value of 0. Until the element reaches that position in the viewport, it will behave as if it has position: relative. Once the top of the element reaches the top of the viewport, position: sticky kicks in, and it behaves as if it has position: absolute.

This is a really cool solution and means you don't have to use JS to create this effect. However, it only works when the navbar's parent is <body>. If that's not possible in your case, don't worry! There's another way...

3) Creating a Sticky Navbar with JS - Initial Positioning

Let's say, for whatever reason, your navbar has to be a child of your #header, but you still want to make it sticky.

You HTML may look something like this:

<section id="header">
<article>
<h2>Let's read some jokes!</h2>
</article>
<nav id="main-nav">
<a href="#header">Home</a>
<a href="#tim-vine">Tim Vine</a>
<a href="#bill-hicks">Bill Hicks</a>
<a href="#stewart-francis">Stewart Francis</a>
</nav>
</section>

Now #main-nav's parent is #header, so if we use position sticky it won't stay in view after we've scrolled past it. For now, let's just position it relatively:

#main-nav {
background-color: #1e1e1e;
padding: 10px;
text-align: center;
width: 100%;
position: relative;
box-sizing: border-box;
height: 45px;
}

At the moment, it's just a standard navbar...

See the Pen on CodePen.

4) Creating a Sticky Navbar with JS - the Sticky Effect

Because the navbar's parent isn't body, we'll actually have to use position: fixed to stick the navbar to the top of the screen.

Let's put this into a class:

#main-nav.sticky {
position: fixed;
top: 0;
}

Next we need to add this class to the #main-nav element when the user scrolls past it.

To know if the visitor has scrolled past the navbar, we need to check two things:

  1. How far the navbar is from the top of the screen
  2. How far the visitor has scrolled

We can find out (1) with the following:

let navbar = document.getElementById("main-nav");
let navPos = navbar.getBoundingClientRect().top;

For (2), we first need to add an event listener to detect scrolling, and then store the scroll position in a variable.

window.addEventListener("scroll", e => {
scrollPos = window.scrollY;
});

Adding the sticky class to the navbar

Great, we now have the information we need. When the user scrolls, we'll check whether the new scroll position (scrollPos) is greater than the navbar's position (navPos) and if so, add the class.

But hold on a second!

When we do this, we're taking a relatively positioned element, and making it absolute. This takes it 'out of the flow' of the markup, and any elements below it will jump upwards to fill the empty space it left behind.

To offset this, we will simply add 45px of padding to the first element after the header, which in this case is the #tim-vine element. Since the navbar is 45px high, this will prevent the content from 'jumping' upwards:

The new class:

.navbarOffsetMargin {
padding-top: 45px;
}

...and the JS that makes it all work:

let timVine = document.getElementById("tim-vine");

window.addEventListener("scroll", e => {
let viewportHeight = window.innerHeight;
let scrollPos = window.scrollY;
if (scrollPos > navPos) {
navbar.classList.add('sticky');
header.classList.add('navbarOffsetMargin');
} else {
navbar.classList.remove('sticky');
header.classList.remove('navbarOffsetMargin');
}
});

Give it a try below!

See the Pen on CodePen.

5) Highlight Active Navbar Sections

With navbars, it's customary to highlight the section of the site that the visitor is currently on.

The logic here is very similar to what we've just done with the navbar. First, we create a class to flip the colors of the navbar link when it's active:

nav a.active {
color: #1e1e1e;
background: white;
}

I also added 5px of padding to nav a, to give it a litle breathing room, and added the active class to the first section - since the user will land on that section:

<a href="#header" class="active">Home</a>

Now, all we have to do is add and remove this class from the a elements within the navbar when the relevant section is in view.

The new JS we need for this is shown below:

let navbarLinks = document.querySelectorAll("nav a");

window.addEventListener("scroll", e => {

scrollpos = window.scrollY;
if (scrollpos > (viewportHeight - navHeight)) {
navbar.classList.add('sticky')
} else {
navbar.classList.remove('sticky')
}

navbarLinks.forEach(link => {
let section = document.querySelector(link.hash);
if (scrollPos + 150 > section.offsetTop && scrollPos + 150 < section.offsetTop + section.offsetHeight ) {
link.classList.add("active");
} else {
link.classList.remove("active");
}
});
});

Going through the new additions line-by-line:

let navbarLinks = document.querySelectorAll("nav a");

This grabs all the a elements within the nav element and puts them into an array called navbarLinks.

navbarLinks.forEach(link => {

We're gonna loop through each of our links, and run the code inside this block. Within this block, we'll use the alias 'link' to refer to our links.

let section = document.querySelector(link.hash);

link.hash refers to the target URL of the link. This line scans the page for an element with that name, and if it finds one, stores it in the section variable.

Since the links point to the sections within the page, this will return the section that the link is pointing to!

Check if the Section is Currently On-Screen

if (scrollPos + 150 > section.offsetTop && scrollPos + 150 < section.offsetTop + section.offsetHeight ) {

This should be familiar to you - it's similar to the logic we used previously. There are two differences though:

  1. This time, we also check whether the bottom of the element (i.e., its offsetTop plus its height) is greater than the scroll position. If it is, that means the user has scrolled past it, so we can remove the active class.

  2. I've added an extra 150 pixels to window.scrollY. Why? Well, imagine you've scrolled 1 pixel above the top of a section.

In this case, your whole screen, apart from that 1 pixel, is taken up by that section. So even though the top of the viewport hasn't officially reached that section, it really feels like that's the one you're "on". Also, remember that 45px of the screen is taken up by the sticky navbar.

Adding the 150px buffer helps adjust for this a bit.

Of course, you don't have to use 150px - use whatever feels right according to the screen size and the amount of content on the page.

     link.classList.add("active");
} else {
link.classList.remove("active");
}
});
});

If the above conditions are met, we'll add the active tag to the link. If not, we remove it. Then we just close our brackets and we're all set!

What if You Want a Truly Fixed Navbar?

You might be wondering, what would you do if you wanted the navbar at the top of the screen all the time - without this sticky effect?

It's easy - just set up the navbar element with position: fixed and top: 0:

#main-nav {
background-color: #1e1e1e;
padding: 10px;
text-align: center;
width: 100%;
position: fixed;
top: 0;
box-sizing: border-box;
height: 45px;
}

Then get rid of the JS that added the sticky class, as we don't need that now. And hey presto!:

See the Pen on CodePen.

6) Throttling the Event Listener

Just one more step before we're finished - if you're using the JS approach, it's a good idea to throttle the functions in the event listener.

Wrong Throttling Meme

No, not that kind of throttling! Here's what I mean...

When the user scrolls, it might trigger 30, 50, maybe even 100 events every second. Not a huge deal in our little project here, but in larger projects with more extensive functions, this could be an unnecessary performance hit.

The solution is throttling. Basically how it works is, every time you call the function, you start a timer. If you try to call the function again before the timer runs out, the throttler won't allow it.

So how do we set this up? Easy - use a library! Lodash is a common one. Just download it and load it in your HTML file:

<script src="lodash.js"></script>

Once that's done, we just pull our code out of the event listener, and into a function:

function toggleClassesOnScroll() {
scrollpos = window.scrollY;
if (scrollpos > (viewportHeight - navHeight)) {
navbar.classList.add('sticky')
} else {
navbar.classList.remove('sticky')
}
navbarLinks.forEach(link => {
let section = document.querySelector(link.hash);
if (section.offsetTop <= window.scrollY + 300 &&
section.offsetTop + section.offsetHeight > window.scrollY + 300) {
link.classList.add("active");
} else {
link.classList.remove("active");
}
});
}

Now, we'd normally call that function from inside the event listener:

window.addEventListener("scroll", e => {
toggleClassesOnScroll()
});

However, with Lodash we just put that function into the throttler, like this:

window.addEventListener("scroll", e => {
_.throttle(toggleClassesOnScroll(), 100);
});

The 100 here just means 100 milliseconds. When our function is called, a 100-millisecond timer starts, and the function won't run again until that timer runs out.

Final Result

See the Pen on CodePen.

Pretty cool eh? By loading the Lodash library, you open up a whole world of performance and tuning functions you can use, to save you having to write all that code by yourself.

Another fantastic library you might want to check out is fullPage.js. This will really take your one-page site to the next level. With fullPage, the different sections of your site are locked at the size of the viewport, and when the user scrolls, they scroll down to the next section (and there are some fancy animations to get them there, as well as smooth scrolling).

This makes everything we've been talking about here much easier - it's easy to check which navbar link is active because one page is active at a time. No need to mess around calculating offsets. So have a look at fullPage.js and see what you think!

About the author:

Warren Davies is a front end developer based in the UK.
You can find more from him at https://warrendavies.net