Creating an Animated Menu Indicator with CSS Selectors
The following article is by James Nowland, a front end developer for Headjam, a creative agency in Newcastle, Australia. James has created a fairly simple little effect here, but one that you might think would require a little JavaScript. Instead, it uses some clever selector usage.
In this article, I’ll cover creative ways of using sibling selectors and pseudo elements to make a CSS-only menu indicator that would normally be achieved using JavaScript.
Here is what we will be making:
See the Pen Step 3 by CSS-Tricks (@css-tricks) on CodePen.
We’ll break this down into three steps:
- Basic structure and styling
- Building the indicator
- Making the indicator move
We’ll also be leveraging SCSS throughout this example to take advantage of the variables and functions Sass offers that make things much easier to maintain in the long-run.
Step 1: Basic structure and styling
First off, let’s set up the HTML for the menu using a basic unordered list structure. We can also mark up the base class names to kick things off.
<ul class="PrimaryNav">
<li class="Nav-item">Home</li>
<li class="Nav-item">About</li>
<li class="Nav-item is-active">Writing</li>
<li class="Nav-item">Clients</li>
<li class="Nav-item">Contact</li>
</ul>
Nothing too fancy so far. We have the
- element with a
PrimaryNav
class name that acts as the container for the list items inside of it, each with a Nav-item
class.
Defining the variables
One of the key features of this navigation is a maximum width that fills the space of a container based on the number of menu items in it. In this case, we will set up a $menu-items
variable in our SCSS which will then be used to calculate the $width
value of each .Nav-item
in the markup.
We’ve also added a $indicator-color
variable to define—you guessed it—the color that will be used for the hover indicator of the menu.
// Menu Item Variables
// The number of items in the menu
$menu-items: 5;
// We multiply it by 1% to get the correct % unit
$width: (100/$menu-items) * 1%;
// Colors
$background-color: #121212;
$indicator-color: #e82d00;
Styling things up
From here, we can create the basic styles for the menu:
// The parent container
.PrimaryNav {
// Remove the bullet points by default
list-style: none;
// Center all the things!
margin: 50px auto;
// The nav will never exceed this width and what our calculated percentages related back to
max-width: 720px;
padding: 0;
width: 100%;
}
// The menu items
.Nav-item {
background: #fff;
display: block;
float: left;
margin: 0;
padding: 0;
text-align: center;
// Our current calculation of 5 items will generate 20%
width: $width;
// The first item in the menu
&:first-child {
border-radius: 3px 0 0 3px;
}
// The last item in the menu
&:last-child {
border-radius: 0 3px 3px 0;
}
// If the menu item is active, give it the same color as the indicator
&.is-active a {
color: $indicator-color;
}
a {
color: $background-color;
display: block;
padding-top: 20px;
padding-bottom: 20px;
text-decoration: none;
&:hover {
color: $indicator-color;
}
}
}
See the Pen Step 1 by CSS-Tricks (@css-tricks) on CodePen.
Step 2: Building the indicator
We’re going to mark this up in a way that uses multiple classes. We could accomplish the same thing using just the .PrimaryNav
class, but adding another class name will allow greater flexibility down the road.
We already have the .PrimaryNav
class that contains the main navigation styling. Now let’s create .with-indicator
to build the indicator:
<ul class="PrimaryNav with-indicator">
</ul>
This is where we can use CSS in place of what we would normally accomplish in JavaScript. We know that adding a class to an element on hover is JavaScript territory, but let’s see how we can do this in CSS alone.
The tricky part is getting the menu items to communicate to each other. In an unordered list, the first list item (:first-child
) can talk to the second child via either sibling selector +
or ~
, but the second child list item cannot talk to the first child (can’t go backwards in the DOM like that in CSS).
See the Pen Step 2 by CSS-Tricks (@css-tricks) on CodePen.
Turns out the best listener out of the list items is the :last-child
. The last child can hear all of the :hover
and :active
states of its siblings. This makes it the perfect candidate for where to set the indicator.
We create the red indicator using the :before
and :after
elements of the last child. The :before
element will use a CSS Triangle and negative margin to center it.
// The hover indicator
.with-indicator {
// The menu is "relative" to the absolute position last-child pseudo elements.
position: relative;
.Nav-item:last-child {
&:before, &:after {
content: '';
display: block;
position: absolute;
}
// The CSS Triangle
&:before {
width: 0;
height: 0;
border: 6px solid transparent;
border-top-color: $color-indicator;
top: 0;
left: 12.5%;
// Fix the offset - may vary per use
margin-left: -3px;
}
// The block that sits behind the text
&:after {
width: $width;
background: $indicator-color;
top: -6px;
bottom: -6px;
left: 0;
z-index: -1;
}
}
}
Step 3: Making the indicator move
Now that the indicator is set up, it needs to be able to move around when a cursor hovers over menu items. Behold the power of the ~
selector, which will be used to match any elements between the first and last children in the markup.
Right now, position:relative
is set on the
- element by default, meaning the indicator sits flush on the first item. We can move the indicator from item to item by modifying the
left
position and—since all the menus are equal width—we know that to move it down one spot the :last-child
selectors for :before
and :after
must have an offset equal to the width of a .Nav-item
. Remember our handy $width
variable? We can use that to on the left
attribute.
This is how we would set that up in vanilla CSS:
.with-indicator .Nav-item:nth-child(1).is-active ~ .Nav-item:last-child:after {
left: 0;
}
.with-indicator .Nav-item:nth-child(2).is-active ~ .Nav-item:last-child:after {
left: 20%;
}
.with-indicator .Nav-item:nth-child(3).is-active ~ .Nav-item:last-child:after {
left: 40%;
}
.with-indicator .Nav-item:nth-child(4).is-active:after {
left: 60%;
}
.with-indicator .Nav-item:nth-child(5).is-active:after {
left: 80%;
}
Let’s make this dynamic with Sass:
// Menu Item Variables
// The number of items in the menu, plus one for offset
$menu-items: 5;
// The actual number of items in the menu
$menu-items-loop-offset: $menu-items - 1;
// We multiply it by 1% to get the correct % unit
$width: (100/$menu-items) * 1%;
.with-indicator {
@for $i from 1 through $menu-items-loop-offset {
// When the .Nav-item is active, make the indicator line up with the navigation item.
.Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:after {
left:($width*$i)-$width;
}
.Nav-item:nth-child(#{$i}).is-active ~ .Nav-item:last-child:before {
left:($width*$i)+($width/2)-$width; /* this ensures the triangle lines up to the menu. */
}
} // end @for loop
It’s worth noting the triangle :before
has an additional half-width offset on top of this left
offset.
Now let’s add some animation and another Sass for
loop so we can initialize where the indicator is, based on the page we are on. When you :hover
over the item the indicator will move. But, once you mouse out it will return to the is-active
state. A nice and neat JavaScript-free way of making a menu indicator.
// This is in a separate loop to avoid having to use "!important" as the hover rules need to be stronger than the ".is-active" class.
@for $i from 1 through $menu-items-loop-offset {
// When the menu is :hover make the indicator line up with it.
.Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:after {
left:($width*$i)-$width;
}
.Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:before{
left:($width*$i)+($width/2)-$width;
}
} // end @for loop
}
The final result
And there we have it! An animated menu indicator without the JavaScript dependency.
See the Pen Step 3 by CSS-Tricks (@css-tricks) on CodePen.
Creating an Animated Menu Indicator with CSS Selectors is a post from CSS-Tricks