How I used modern CSS features to improve the UX of Coding Kittens Navbar

January 20, 2024

Warning! Experimental feature

At the time of writing this post animation-timeline:scroll feature is still experimental, so make sure you add a fallback. Check the compatibility list in caniuse.com

I have worked on many iterations of the navigation bar on this website to achieve a style that I was satisfied with. I wanted a logo that stood out and a navigation always visible — especially for mobile — so that the person reading this post could interact with it at all times.

For this reason I decided that the navigation bar should stick to the top on user scroll and I added the site’s logo, name and tagline.

Beautiful, — from my point of view — I was finally happy with the result.

git commit -m "implement absolute perfect non-AI Navbar"

After the automatic deploy, I grab my phone and navigate to my site. Everything was looking good until I started reading my first post How to fix Stack-Size limit error of a recursive function using iteration in JavaScript and I realized that on my tiny phone with my tiny screen the navbar was occuping a lot of the reading space.

Back then I had a 4.7 inch phone so I usually didn’t have much space to read.

Look at this.

Coding Kittens website with navbar occuping a big percentage of the screen

The Navbar takes a big percentage of the screen on a small device

I feel something similar to claustrophobia when I have the text contained within such a small space.

However, I didn’t want the navigation bar to stand out less — the first impression when you land into a website is the most important — but I couldn’t leave it like that.

Then I thought… “Okay, if it bothers me when I scroll down to read the blog post then… How about I make the navigation bar smaller when the user is not at the top of the page?”

And that’s what I did.

Ok, context given, now let’s travel to the land of CSS and JavaScript, where everything is possible and there are many different ways of doing the same thing. 🪄🔮

If you do a quick Google search on how to shrink the navigation menu as the user scrolls down, the first result that will come up will surely be this page from W3schools: How TO - Shrink Navigation Menu on Scroll .

The HTML and CSS sections are basic layout of a navbar. The interesting part is this JavaScript piece.

// When the user scrolls down 80px from the top of the document, resize the navbar's padding and the logo's font size
window.onscroll = function () {
  scrollFunction();
};
 
function scrollFunction() {
  if (document.body.scrollTop > 80 || document.documentElement.scrollTop > 80) {
    document.getElementById('navbar').style.padding = '30px 10px';
    document.getElementById('logo').style.fontSize = '25px';
  } else {
    document.getElementById('navbar').style.padding = '80px 10px';
    document.getElementById('logo').style.fontSize = '35px';
  }
}

This code basically assigns a function handler to the global onscroll event and checks what proportion of the <body> or the <html> the user has scrolled. It will apply one set of styles or the other depending on the scroll offset.

That’s all, thanks for reading this post.

Now seriously, this attempt to fix the problem is the simplest, and probably a good option in most cases.

The problem is… where is the fun in doing it like this? Coding Kittens is my space to experiment and try new things. I wanted to find something more performant, extensible and CSS-only.

What if I told you that there’s a futuristic way of approaching this by using only CSS? Would you take the boring pill or the future pill ?

If you are curious I will explain in the next section an experimental function that will help us implement this functionality.

Most of the functionalities that we will use have 96% support from browsers according to the website Can I use . On the other hand, the important functionality, animation-timeline:scroll, is less supported, but we will explain how to add an alternative for browsers that do not support it.

So, we will start by explaining what animation-timeline:scroll is.

I’ll be honest, at the time of writing this I was going to give a detailed explanation of how it works, but I have realized that everything I can say has already been said as it should in different sources where I have found this information.

A Scroll Progress Timeline is an animation timeline that is linked to progress in the scroll position of a scroll container–also called scrollport or scroller–along a particular axis. It converts a position in a scroll range into a percentage of progress.

To put things simple, it allows us to trigger an animation based on the scroll position. Just with CSS.

Integrating scroll-driven animations with two existing APIs, means that they benefit from the advantages of these APIs. That includes the ability to have these animations run off the main thread. Yes, read that correctly: you can now have silky smooth animations, driven by scroll, running off the main thread, with just a few lines of extra code. What’s not to like?!

This is mind blowing 🤯

It means that this animation will not block the main thread, bye junky animations!

Check out the performance case study linked above for cool performance comparisons between classic JavaScript vs animation-timeline.

Now let’s begin to do some magic.

In the following example I have created a navigation bar similar to the one I created for the Coding Kittens website. I’ll start with the basic HTML.

index.html
... html code here
<nav class="navbar">
  <div class="backdrop"></div>
  <div class="navbar-content">
    <a href="/" class="logo-link">
      <!--Logo SVG or image goes here -->
      <div class="site-name">
        <p>Your site</p>
      </div>
    </a>
    <div class="navigation-links-container">
      <div class="navigation-links">
        <a>Home</a>
        <a>Blog</a>
      </div>
    </div>
  </div>
</nav>
... more html code here

Next, we’ll need to add some styling to this element to make it similar to a navigation bar.

styles.css
.navbar {
  display: flex;
  position: sticky;
  padding: 1rem 0.5rem;
  margin-bottom: 3rem;
  margin-top: 5rem;
  flex-direction: row;
  align-items: flex-start;
  top: 0;
 
  @media (min-width: 768px) {
    padding: 1rem 1.25rem;
  }
}
 
.backdrop {
  position: absolute;
  top: 0;
  min-width: 100%;
  height: 5rem;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  left: -8px;
  right: -8px;
 
  @media (min-width: 768px) {
    margin-right: -28px;
    margin-left: -28px;
  }
}
 
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}
 
.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}
 
.logo {
  margin-right: 1rem;
}
 
.site-name {
  display: flex;
  flex-direction: column;
}
 
.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;
 
  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}
 
.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;
 
  @media (min-width: 640px) {
    flex-direction: row;
  }
}
 
.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;
 
  @media (min-width: 640px) {
    display: flex;
  }
}
 
.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

Once we have all these pieces together we will have the following:

.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
  }
  
  .backdrop {
    position: absolute;
    top: 0;
    min-width: 100%;
    height: 5rem;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    left: -8px;
    right: -8px;
  
    @media (min-width: 768px) {
      margin-right: -28px;
      margin-left: -28px;
    }
  }
  
  
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}

.logo {
  margin-right: 1rem;
}

.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}

.site-name {
  display: flex;
  flex-direction: column;
}

.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;

  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}

.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;

  @media (min-width: 640px) {
    flex-direction: row;
  }
}

.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;

  @media (min-width: 640px) {
    display: flex;
  }
}

.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

.scrollable-container {
  min-height: 150dvh;
}

body {
  font-family: sans-serif;
  -webkit-font-smoothing: auto;
  -moz-font-smoothing: auto;
  -moz-osx-font-smoothing: grayscale;
  font-smoothing: auto;
  text-rendering: optimizeLegibility;
  font-smooth: always;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;

  background-color: color-mix(in srgb, slategrey 100%, black 100%);
  color: white;
}

We have our first version of the navigation bar, similar to the one mentioned at the beginning of this blog post . We haven’t added the animation yet and it takes up a large percentage of the screen. We can do it better.

We will apply some CSS spells to it. 🪄

styles.css
  @keyframes shrink {
    0% {
      --navbar-shrink: 0;
    }
    10%,
    100% {
      --navbar-shrink: 1;
    }
  }
 
.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
 
    --navbar-shrink: 0; 
    animation: shrink; 
    animation-timing-function: ease; 
    animation-timeline: scroll(nearest block); 
}

I will explain step by step what we are doing here:

  1. First we define the keyframes of our animation. The keyframes will change a CSS variable that we have called --navbar-shrink. We’ll use that value as our conditional value to toggle between shortened or normal navigation.

  2. Next we initialize --navbar-shrink to 0

  3. Then we assign shrink to the animation

  4. As timing function we will use ease. But you can use whatever value you want from this list

  5. Finally we define animation-timeline to call the CSS function scroll with nearest as scrollable and block as axis. We will apply some CSS magic tricks

Remember

animation-timeline must be declared after the animation shorthand as the shorthand will reset non-included longhands to their initial value.

The code we just added allows us to use the conditional CSS variable to apply different states to our navigation bar based on the user’s scroll position. How cool is that?

Now let’s add some juicy animations:

styles.css
... our .navbar styles... .navbar .navbar-content {
  transition: transform 0.3s ease;
  transform: translateY(calc(0px - var(--navbar-shrink) * 12px));
  /* calc(initial_position - (0 * 12 || 1 * 12))  */
}
.navbar .backdrop {
  transition: transform 0.3s ease;
  transform: translateY(calc(0px - var(--navbar-shrink) * 24px));
  /* calc(initial_position - (0 * 24 || 1 * 24))  */
}
 
.navbar .logo {
  transition: transform 0.3s ease;
  transform: scale(calc(1 - var(--navbar-shrink) * 0.2));
  /* calc(initial_scale - (0 * 0.2 || 1 * 0.2))  */
}
 
.navbar p {
  transition: opacity 0.3s ease;
  opacity: calc(1 - var(--navbar-shrink));
  /* calc(initial_opacity - (0 || 1))  */
}
... more styles ...;

As we can see we use the --navbar-shrink variable to dynamically calculate the value using the CSS calc function. This is what it is doing:

  • Move the navbar-content and backdrop several pixels off the screen.

  • Scale down the logo

  • Reduce the text opacity

This code can be extended to include all the transitions we want.

Notice how I used scale, opacity and translateY ​​​​to take advantage of composite animation .

As I mentioned at the beginning of this post, the animation-timeline CSS property is not yet supported in all browsers. In fact, in January 2024 only the Chromium and Firfeox browsers (using an experimental configuration option) support this property.

Don’t be afraid, I have the ideal solution for you. We’ll make some changes to our CSS and add some JavaScript.

We’ll start by adding the “CSS @supports” rule to check if the browser supports the scroll() function.

styles.css
.navbar {
  display: flex;
  position: sticky;
  padding: 1rem 0.5rem;
  margin-bottom: 3rem;
  margin-top: 5rem;
  flex-direction: row;
  align-items: flex-start;
  top: 0;
 
  @media (min-width: 768px) {
    padding: 1rem 1.25rem;
  } 
 
   --navbar-shrink: 0;

    animation: shrink;
    animation-timeline: scroll(nearest block);
    animation-timing-function: ease;
}
 
  @supports (animation-timeline: scroll()) {
    .navbar {
      --navbar-shrink: 0;

      animation: shrink;
      animation-timeline: scroll(nearest block);
      animation-timing-function: ease;
    }
  }
    

Next, if there is no support, we need to toggle the --navbar-shrink variable anyway, since we still need to dynamically change the styles to the navbar elements. navigation.

We’ll add some JavaScript below our nav element to be executed after the element is rendered on the page. Ideally, it can also be added just before the closing of the </body> tag.

index.html
... html code here
<nav class="navbar">
  <div class="backdrop"></div>
  <div class="inner-container">
    <a href="/" class="logo-link">
      <!--Logo SVG or image goes here -->       
      <div class="site-name">
        <p>Your site</p>
      </div>
    </a>
    <div class="navigation-links">
      <div class="navigation-links-inner">
        <a>Home</a>
        <a>Blog</a>
      </div>
    </div>
  </div>
</nav>
<script>
  (function () {
    const navbar = document.querySelector(".navbar");
 
    if (!CSS.supports("animation-timeline: scroll()")) {
      const threshold = 500; // Adjust this threshold as needed
 
      window.addEventListener("scroll", handleScroll);
 
      function handleScroll() {
        if (!navbar) {
          return;
        }
 
        const scrollY = window.scrollY || window.pageYOffset;
 
        if (scrollY > threshold) {
          navbar.style.setProperty("--navbar-shrink", "1");
        } else {
          navbar.style.setProperty("--navbar-shrink", "0");
        }
      }
    }
  })();
</script>
... more html code here

This code executes the implementation that I have shown you at the beginning of this entry .

If you want, you can check the result of the changes we have applied in the following Code Sandbox:

.navbar {
    display: flex;
    position: sticky;
    padding: 1rem 0.5rem;
    margin-bottom: 3rem;
    margin-top: 5rem;
    flex-direction: row;
    align-items: flex-start;
    top: 0;
  
    @media (min-width: 768px) {
      padding: 1rem 1.25rem;
    }
  }
  
  .backdrop {
    position: absolute;
    top: 0;
    min-width: 100%;
    height: 5rem;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    left: -8px;
    right: -8px;
  
    @media (min-width: 768px) {
      margin-right: -28px;
      margin-left: -28px;
    }
  }
  
 .navbar .navbar-content {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: translateY(calc(0px - var(--navbar-shrink) * 12px));
}
.navbar .backdrop {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: translateY(calc(0px - var(--navbar-shrink) * 24px));
}

.navbar .logo {
  transition: transform 0.3s ease;
  will-change: transform;
  transform: scale(calc(1 - var(--navbar-shrink) * 0.2));
}

.navbar .site-name > p {
  transition: opacity 0.3s ease;
  opacity: calc(1 - var(--navbar-shrink));
}

@supports (animation-timeline: scroll()) {
  .navbar {
    --navbar-shrink: 1;

    animation: shrink;
    animation-timeline: scroll(nearest block);
    animation-timing-function: ease;
  }
}

@keyframes shrink {
  0% {
    --navbar-shrink: 0;
  }
  10%,
  100% {
    --navbar-shrink: 1;
  }
}
  
.navbar-content {
  display: flex;
  z-index: 10;
  margin-left: 0;
  flex-direction: row;
  justify-content: space-between;
  min-width: 100%;
}

.logo {
  margin-right: 1rem;
}

.logo-link {
  display: flex;
  flex-direction: row;
  align-items: center;
  text-decoration: none;
}

.site-name {
  display: flex;
  flex-direction: column;
}

.site-name > p {
  color: white;
  margin: 0;
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: 700;

  @media (min-width: 640px) {
    font-size: 1.5rem;
    line-height: 2rem;
  }
}

.navigation-links-container {
  display: flex;
  flex-direction: row-reverse;
  gap: 1.5rem;

  @media (min-width: 640px) {
    flex-direction: row;
  }
}

.navigation-links {
  display: none;
  flex-direction: row;
  align-items: center;

  @media (min-width: 640px) {
    display: flex;
  }
}

.navigation-links a {
  position: relative;
  padding: 0.25rem 0.5rem;
}

.scrollable-container {
  min-height: 150dvh;
}

body {
  font-family: sans-serif;
  -webkit-font-smoothing: auto;
  -moz-font-smoothing: auto;
  -moz-osx-font-smoothing: grayscale;
  font-smoothing: auto;
  text-rendering: optimizeLegibility;
  font-smooth: always;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;

  background-color: color-mix(in srgb, slategrey 100%, black 100%);
  color: white;
}

That’s all folks! 🥕

We’ve added some tasteful touches to the UX to improve the experience for users using devices with small screens by applying a navigation bar animation that performs better than the traditional variant, is more extensible and uses CSS. We have also added an alternative for those users who use unsupported browsers.

I encourage you to experiment with the Code Sandbox by adding more keyframes, more CSS variables and ultimately more variations.

I hope you enjoyed this post. See you soon! 😄