Four methods to keep a navbar at the top of the screen.

Last update: September 12, 2019

In this post, you’ll see 4 methods you can use to keep a navigation bar at the top of the screen when the user scrolls down the page. This can be useful for single-page applications where the pages tend to be long, and you want to give the user the option to jump from section to section without having to go all the way up.

GitHub repository.

Table of contents

Position fixed

In the past, the easier way to achieve this was to give the element a position: fixed and place it at the top-left of the screen. For example:

.navbar {
  position: fixed;
  width: 100%;
  top: 0;
  left: 0;
}

This code removes the navbar from the normal content flow and places it at the top of the screen. As a result, the rest of the content tries to fill up the space the navbar left and goes under it. If you want to avoid hiding elements behind the navbar, you can either add a top margin to the rest of the content—equal to the navbar height—or you can make the navbar transparent.

Scroll event

If you want some kind of transition/animation, you can give the navbar a default relative position and then create a listener for the scroll event. Inside the scroll listener, you have the following logic: if the distance from the top of the screen is greater (further down) than the navbar height, you add a “scrolling” class to navbar where you change (with CSS) the position to fixed and the properties you want to transition. Consider the following CSS code:

/* default state */
.navbar {  position: relative;  /* default colors + transition */
  background-color: white;
  color: black;
  transition: all 0.3s ease-out;
}

/* scrolling state */
.navbar.fixed-top {  position: fixed;  top: 0;  width: 100%;
  /* scrolling colors */
  background-color: black;
  color: white;
}

.navbar.fixed-top a {
  color: white;
}

and in JavaScript:

document.addEventListener("scroll", function() {
  const navbar = document.querySelector(".navbar");
  const navbarHeight = 100;

  const distanceFromTop = Math.abs(document.body.getBoundingClientRect().top);

  if (distanceFromTop >= navbarHeight) navbar.classList.add("fixed-top");  else navbar.classList.remove("fixed-top");});

Intersection observer

A more modern and performant approach to this problem is to use an intersection observer instead of a scroll listener. First, you add a zero-height div below the navbar, and you give it a .spot class.

<nav class="navbar">
  <ul>
    <li><a href="/">Position fixed</a></li>
    <li><a href="/scroll.html">Scroll event</a></li>
    <li><a href="/observer.html">Intersection observer</a></li>
    <li><a href="/sticky.html">Position sticky</a></li>
  </ul>
</nav>
<div class="spot"></div>

Then, you initialize an IntersectionObserver and start observing that spot element with observer.observe(spot):

// element references
const navbar = document.querySelector(".navbar");
const spot = document.querySelector(".spot");

// initialize and start the observer.
const observer = new IntersectionObserver(handleScroll, options);observer.observe(spot);

When you initialize the observer, you pass some options and a handleScroll method:

// element references
const navbar = document.querySelector(".navbar");
const spot = document.querySelector(".spot");

// handler
const handleScroll = entries => {  const spotIsVisible = entries[0].isIntersecting;  if (spotIsVisible) navbar.classList.remove("fixed-top");  else navbar.classList.add("fixed-top");};// optionsconst options = {  root: null, // null means that root element = browser viewport.  rootMargin: "0px", // margin around the browser viewport.  threshhold: 0 // see below what 0 means.};
// initialize and start the observer.
const observer = new IntersectionObserver(handleScroll, options);
observer.observe(spot);

The handler and the options give the following behavior: If the spot is completely hidden (not intersecting), we add a “scrolling” class to the navbar; if even 1px is visible (intersecting), we remove that class. handleScroll method is called in both cases.

Caveats

  • Both of the last two methods (intersection observer/scroll listener) can cause the page to momentary jump when the transition happens, due to the removal/addition of the navbar from the normal content flow.
  • If the .spot div has height other than 0, or if you observe the navbar instead of the spot, you may get an infinite loop where you add/remove the fixed-top class from the navbar.

Position sticky

The last option you have is my favorite. It’s easy as the first method but without the “margin drawback”. Additionally, the transition is smooth and doesn’t cause that jump I described earlier. You achieve that by giving the navbar a position: sticky; and place it at the top of the screen:

.navbar {
  position: sticky;  top: 0;  background-color: transparent;
}

#content {
  background-color: aquamarine;
}

I also made the navbar transparent and the content aquamarine to prove that the content doesn’t go under the navbar. If you want to add a transition, you can register an intersection observer, but this time, you don’t have to change the position when the navbar is “fixed-top”.

Position sticky is supported by all the major browsers.

Other things to read

Popular notes

Other posts