Build a Navigation Menu With an Animated Active Indicator (JavaScript)
In a previous tutorial, we discussed how to build a shifting underline hover effect. Today, we’ll learn how to create another fancy menu effect: each time we click on, or hover over an item (the choice is yours!) there will be a magic moving element that will follow along with the active item.
During this journey, we'll also cover a useful technique to update CSS pseudo-elements styles through JavaScript with the help of the CSS variables.
Navigation Demos
Take a look at the first demo where the animation will happen on click:
Then the second one where the animation will happen on hover:
1. Begin With the HTML Markup
To develop this component, we’ll define a nav
element that will contain the menu wrapped inside a container. By default, we’ll give the active
class to the first list item, but we can equally assign this class to any of them.
Here’s the required structure:
1 |
<div class="container"> |
2 |
<nav>
|
3 |
<ul class="menu"> |
4 |
<li class="active"> |
5 |
<a href="">Home</a> |
6 |
</li>
|
7 |
<li>
|
8 |
<a href="">Information About Us</a> |
9 |
</li>
|
10 |
<li>
|
11 |
<a href="">Clients</a> |
12 |
</li>
|
13 |
<li>
|
14 |
<a href="">Contact</a> |
15 |
</li>
|
16 |
</ul>
|
17 |
</nav>
|
18 |
</div>
|
2. Add the CSS
Happily enough, we’ll only need a few styles.
As we said, the menu will live inside a container with a maximum width of 1000px.
To create the element with the moving background that will act as an active menu indicator, we won’t use any extra HTML elements. Instead, we’ll define the ::before
pseudo-element of the menu and then update its transform
and width
values dynamically through JavaScript.



On screens smaller than 801px, we’ll hide the moving highlighter and just use some similar styles to indicate the active menu item.
Here are the menu styles that mainly interest us:
1 |
/*CUSTOM VATIABLES HERE*/
|
2 |
|
3 |
.menu { |
4 |
list-style: none; |
5 |
position: relative; |
6 |
display: inline-flex; |
7 |
background: var(--pink); |
8 |
padding: 10px; |
9 |
border-radius: 15px; |
10 |
box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; |
11 |
}
|
12 |
|
13 |
.menu::before { |
14 |
content: ""; |
15 |
position: absolute; |
16 |
top: 10px; |
17 |
left: 0; |
18 |
transform: translateX(var(--transformJS)); |
19 |
width: var(--widthJS); |
20 |
height: calc(100% - 20px); |
21 |
border-radius: var(--active-link-border-radius); |
22 |
background: var(--light-pink); |
23 |
box-shadow: var(--active-link-box-shadow); |
24 |
transition: all 0.3s linear; |
25 |
}
|
26 |
|
27 |
.menu li a { |
28 |
display: inline-block; |
29 |
position: relative; |
30 |
padding: 10px 20px; |
31 |
font-size: 20px; |
32 |
font-weight: 500; |
33 |
z-index: 1; |
34 |
}
|
35 |
|
36 |
.menu li:not(:last-child) { |
37 |
margin-right: 20px; |
38 |
}
|
39 |
|
40 |
@media (max-width: 800px) { |
41 |
.menu, |
42 |
.menu li { |
43 |
display: inline-block; |
44 |
}
|
45 |
|
46 |
.menu li.active a { |
47 |
background: var(--light-pink); |
48 |
border-radius: var(--active-link-border-radius); |
49 |
box-shadow: var(--active-link-box-shadow); |
50 |
}
|
51 |
|
52 |
.menu::before { |
53 |
display: none; |
54 |
}
|
55 |
}
|
3. Apply the JavaScript
Now for the interesting part.
We’ll specify the doCalculations()
function that will get as a parameter the active item and do these things:
- Calculate its width and offset left position relative to the parent list.
- Update accordingly the
transformJS
andwidthJS
CSS variables that will in their turn set thetransform
andwidth
values of the menu’s::before
pseudo-element.
This function will run in the following cases:
- When the DOM is ready. In such a case, the active item will be the one with the
active
class. By default, this will be the first one. - Each time we click on or hover over a menu link.
- As we resize the browser window. This is important because remember that on smaller screens we follow a different approach.
Here’s the required JavaScript code for the on click animation:
1 |
const menu = document.querySelector(".menu"); |
2 |
const menuLinks = menu.querySelectorAll("a"); |
3 |
const menuLinkActive = menu.querySelector("li.active"); |
4 |
const activeClass = "active"; |
5 |
|
6 |
doCalculations(menuLinkActive); |
7 |
|
8 |
for (const menuLink of menuLinks) { |
9 |
menuLink.addEventListener("click", function (e) { |
10 |
e.preventDefault(); |
11 |
menu.querySelector("li.active").classList.remove(activeClass); |
12 |
menuLink.parentElement.classList.add(activeClass); |
13 |
doCalculations(menuLink); |
14 |
});
|
15 |
}
|
16 |
|
17 |
function doCalculations(link) { |
18 |
menu.style.setProperty("--transformJS", `${link.offsetLeft}px`); |
19 |
menu.style.setProperty("--widthJS", `${link.offsetWidth}px`); |
20 |
}
|
21 |
|
22 |
window.addEventListener("resize", function() { |
23 |
const menuLinkActive = menu.querySelector("li.active"); |
24 |
doCalculations(menuLinkActive); |
25 |
});
|
For the on hover animation, we’ll replace the click
event with the mouseenter
one like this:
1 |
...
|
2 |
|
3 |
menuLink.addEventListener("mouseenter", function () { |
4 |
document.querySelector(".menu li.active").classList.remove(activeClass); |
5 |
menuLink.parentElement.classList.add(activeClass); |
6 |
doCalculations(menuLink); |
7 |
});
|
Conclusion
Done! As quickly as this, we managed to develop a practical navigation menu animation that can occur either on click or hover. A more advanced implementation of it puts dropdowns into play and builds follow-along navigation like on Stripe’s website.
Apart from the animation itself, another thing to keep in mind is the way we used CSS variables to update the pseudo-elements styles. This is a great source of knowledge in cases you have headaches from manipulating pseudo-elements through JavaScript.
Once again, let’s recall our demos.
The first demo:
And the second one:
As always, thanks a lot for reading!