I've never had the chance to use this kind of menu on my regular projects, so I decided to make one, just for fun. I was really happy with the final result, so I decided to share it with the community. Even though I created this one using React, I wanted to make one with vanilla JavaScript so you can use it on any project.
HTML setup
Let's start off by creating a simple navigation markup with a simple list with a few links. We'll add id
attributes to the root nav
element and a div
element that we'll use as a pointer. We'll need a few classes for the nav, list, and pointer elements so we can style them.
<nav class="nav" id="js-nav">
<div id="js-pointer" class="nav__pointer"></div>
<ul class="nav__list">
<li><a href="#">Overview</a></li>
<li><a href="#">Goals</a></li>
<li><a href="#">Inspiration</a></li>
<li><a href="#">Profile</a></li>
</ul>
</nav>
CSS markup
Let's add some styles. The following code snippet shows only required styles.
We need to position nav
element relatively and add some padding. We'll need that value for JavaScript. We'll position nav__pointer
absolutely with a z-index
value that is lower than nav__list
so the indicator is positioned under the links.
We need to position the links in a 4 * 1fr
column grid so all link containers are equal width.
.nav {
position: relative;
padding: 1em;
}
.nav__pointer {
z-index: 1;
position: absolute;
top: 0.6em;
left: 1em;
background-color: #bada55;
height: 1.8em;
transition: transform 0.25s ease-in-out;
border-radius: 0.3em;
will-change: transform;
backface-visibility: hidden;
}
.nav__list {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: repeat(4, 1fr);
}
JavaScript
We need to select our navigation, indicator, and link elements with JavaScript. Remember that 1em
value padding from CSS? We'll use half of that value so we can position the pointer appropriately.
To avoid using magic numbers, we'll dynamically calculate the indicator's width depending on the number of columns in the grid (how many links there are).
var CONTAINER_PADDING_HALF = "0.5em";
pointer.style.width = "calc(100% /"+links.length+" - "+CONTAINER_PADDING_HALF+")"
For each selected link within the nav
element, we'll add a data
attribute that stores a percentage value based on the order. If the link is first in the list, it will have a 0%
value, if it's a second, it will have a 100%
value, etc. We'll use those values for transforms.
We're also attaching a click event listener for each link.
for(var i=0; i<links.length; i++){
var current = links[i];
current.dataset.order = i * 100 + "%";
current.addEventListener("click", movePointer);
}
Our link click event handler is very simple - it only applies a CSS transform attribute to the navigation indicator. The value that is being applied depends on data-order
attribute that we've set.
function movePointer(e) {
var order = e.currentTarget.dataset.order;
pointer.style.transform = "translate3d("+order+",0,0)"
}
Since the width of the indicator matches the width of the each navigation link in the grid, and we've positioned the indicator absolutely and to the start of the first link, we only have to apply transforms in 100%
increments. First link will have the 0%
value, second link will have the 100%
value, third link will have the 200%
value, etc.
By using 3D CSS transforms, this animation will be GPU-powered and it will be smooth and performant.
Final Result
Here is the CodePen link to the final result.
These articles are fueled by coffee. So if you enjoy my work and found it useful, consider buying me a coffee! I would really appreciate it.
Thank you for taking the time to read this post. If you've found this useful, please give it a ❤️ or 🦄, share and comment.
Top comments (1)
❤️ 🦄