Creating an Animated Menu Indicator with CSS Selectors

Avatar of James Nowland
James Nowland on (Updated on )

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

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 <ul> 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 <ul> 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 leftoffset.

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.

// We had to use !important to make the hovers overide for when the :last-child is-active or hovered
@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 !important;
  }

  .Nav-item:nth-child(#{$i}):hover ~ .Nav-item:last-child:before{
    left:($width*$i)+($width/2)-$width !important;
  }
} // end @for loop

// make sure the last-child talks to itself
.Nav-item {
  &:last-child {
    &:hover, &.is-active {
      &:before {
        left: (100%-$width)+($width/2) !important;
      }
      &:after{
        left: 100%-$width !important;
      }
    }        
  }
}

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.