This content originally appeared on Envato Tuts+ Tutorials and was authored by George Martsoukos
In today’s tutorial, we’re going to use a little bit of CSS and JavaScript to create a fancy menu hover effect. It’s not a complicated end result, yet building it will be a great opportunity to practice our front-end skills.
Without further intro, let’s check out what we’ll be building:
The Markup
We start with some very basic markup; a nav
element which contains the menu and an empty span
element:
<nav class="mynav"> <ul> <li> <a href="">Home</a> </li> <li> <a href="">About</a> </li> <li> <a href="">Company</a> </li> <li> <a href="">Work</a> </li> <li> <a href="">Clients</a> </li> <li> <a href="">Contact</a> </li> </ul> </nav> <span class="target"></span>
The CSS
With the markup ready, next we specify some basic styles for the related elements:
.mynav ul { display: flex; justify-content: center; flex-wrap: wrap; list-style-type: none; padding: 0; } .mynav li:not(:last-child) { margin-right: 20px; } .mynav a { display: block; font-size: 20px; color: black; text-decoration: none; padding: 7px 15px; } .target { position: absolute; border-bottom: 4px solid transparent; z-index: -1; transform: translateX(-60px); } .mynav a, .target { transition: all .35s ease-in-out; }
Notice that the span
element (.target
) is absolutely positioned. As we’ll see in a moment, we’ll use JavaScript to determine its exact position. In addition, it should appear behind the menu links, so we give it a negative z-index
.
The JavaScript
At this point, let’s focus our attention on the required JavaScript. To begin with, we target the desired elements. We also define an array of colors which we’ll use later.
const target = document.querySelector(".target"); const links = document.querySelectorAll(".mynav a"); const colors = ["deepskyblue", "orange", "firebrick", "gold", "magenta", "black", "darkblue"];
Events
Next we listen for the click
and mouseenter
events of the menu links.
When the click
event happens, we prevent the page from reloading. Of course, this works in our case because all links have an empty href
attribute. In a real project however, each of the menu links would likely open a different page.
Most importantly, as soon as the mouseenter
event fires, the mouseenterFunc
callback function is executed:
for (let i = 0; i < links.length; i++) { links[i].addEventListener("click", (e) => e.preventDefault()); links[i].addEventListener("mouseenter", mouseenterFunc); }
mouseenterFunc
The body of the mouseenterFunc
function looks like this:
function mouseenterFunc() { for (let i = 0; i < links.length; i++) { if (links[i].parentNode.classList.contains("active")) { links[i].parentNode.classList.remove("active"); } links[i].style.opacity = "0.25"; } this.parentNode.classList.add("active"); this.style.opacity = "1"; const width = this.getBoundingClientRect().width; const height = this.getBoundingClientRect().height; const left = this.getBoundingClientRect().left; const top = this.getBoundingClientRect().top; const color = colors[Math.floor(Math.random() * colors.length)]; target.style.width = `${width}px`; target.style.height = `${height}px`; target.style.left = `${left}px`; target.style.top = `${top}px`; target.style.borderColor = color; target.style.transform = "none"; }
Inside this function we do the following:
- Add the
active
class to the immediate parent (li
) of the target link. - Decrease the
opacity
from all menu links, apart from the “active” one. - Use the
getBoundingClientRect
method to retrieve the size of the associated link and its position relative to the viewport. - Get a random color from the aforementioned array and pass it as value to the
border-color
property of thespan
element. Remember, its initial property value is set totransparent
. - Assign the values extracted from the
getBoundingClientRect
method to the corresponding properties of thespan
element. In other words, thespan
tag inherits the size and the position of the link that’s being hovered over. - Reset the default transformation applied to the
span
element. This behavior is only important the first time we hover over a link. In this case, the transformation of the element goes fromtransform: translateX(-60px)
totransform: none
. That gives us a nice slide-in effect.
If Active
It's important to note that the code above is executed every time we hover over a link. It therefore runs when we hover over an “active” link as well. To prevent this behavior, we wrap the code above inside an if
statement:
function mouseenterFunc() { if (!this.parentNode.classList.contains("active")) { // code here } }
So far, our demo looks as follows:
Nearly, but Not Quite
So, everything seems to work as expected, right? Well, that's not true because if we scroll through the page, or resize the viewport, and then try to select a link, things get messy. Specifically, the position of the span
element becomes incorrect.
Play around with the full page demo (make sure you’ve added enough dummy content) to see what I mean.
To solve it, we have to calculate how far we’ve scrolled from the top of the window and add this value to the current top
value of the target element. In the same way we should calculate how far the document has been scrolled horizontally (just in case). The resulting value is added to the current left
value of the target element.
Here are the two lines of code that we update:
const left = this.getBoundingClientRect().left + window.pageXOffset; const top = this.getBoundingClientRect().top + window.pageYOffset;
Keep in mind that all the code above is executed as soon as the browser processes the DOM and finds the relevant script. Again, for your own implementations and designs you might want to run this code when the page loads, or something like that. In such a scenario, you’ll have to embed it within an event handler (e.g. load
event handler).
Viewport
The last thing we have to do is to ensure that the effect will still work as we resize the browser window. To accomplish this, we listen for the resize
event and register the resizeFunc
event handler.
window.addEventListener("resize", resizeFunc);
Here’s the body of this handler:
function resizeFunc() { const active = document.querySelector(".mynav li.active"); if (active) { const left = active.getBoundingClientRect().left + window.pageXOffset; const top = active.getBoundingClientRect().top + window.pageYOffset; target.style.left = `${left}px`; target.style.top = `${top}px`; } }
Inside the function above, we do the following:
- Check if there’s a menu list item with the class of
active
. If there is such an element, that states that we’ve already hovered over a link. - Get the new
left
andtop
properties of the “active” item along with the related window properties and assign them to thespan
element. Note that we retrieve the values only for the properties that change during theresize
event. That means, there’s no need to recalculate the width and height of the menu links.
Browser Support
The demo works well in all recent browsers. If you encounter any issues though, let me know in the comments below. Also, as you’ve possibly noticed, we use Babel to compile our ES6 code down to ES5.
Conclusion
In this tutorial we went through the process of creating a simple, yet interesting menu hover effect.
I hope you enjoyed what we built here and took inspiration for developing even more powerful menu effects like the one appearing (at the time of writing) in the Stripe site.
Have you ever created something similar? If so, be sure to share with us the challenges you faced.
This content originally appeared on Envato Tuts+ Tutorials and was authored by George Martsoukos
George Martsoukos | Sciencx (2017-03-22T20:34:33+00:00) How to Build a Shifting Underline Hover Effect With CSS and JavaScript. Retrieved from https://www.scien.cx/2017/03/22/how-to-build-a-shifting-underline-hover-effect-with-css-and-javascript/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.