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 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.
// 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.
Pretty cool effect; and some good example of how sass can be used.
I did something similar a while ago but its not as nice as yours and uses a nasty non semantic element to create the indicator so I like your method much better :)
Good job and nice write up.
shows how sass can be used effectively, anyway it seems a guest post on css-tricks
way2sms
I tried it out , and :
...
&:after {
...
bottom :-6px;
...
}
...
didn’t worked like planed, it worked only when I set it to -66px.
Don’t know why? (tried it in chrome and firefox)
Missing a clearfix me thinks
Thnx, Dave , there are global resets that here are not shown and that’s why it didn’t work properly.
This doesn’t work for me in latest Chrome on an iOS 9 iPad. I have to return to the Contact tab each time, then click a different tab to see a transition.
Any ideas how to make it work for different menu item widths?
It’s far easier to do this with fixed percentages as done in the demo and it’s more responsive.
If you know the exact widths of all your link items you could in theory hard coded widths like:
If you did hardcode lots of items like that though you would dreading any updates.
I personally would look at using javascript if there was lots of varying unknown nav item widths.
Pretty, but if you using .is-active indicator class, you have to use little javascript to add the class (or :active CSS selector), don’t you? Is there any magic? Thanks
The
is-active
class can be set by the server on page load. As this demo isn’t reloading pages it’s less obvious.There are a few ways to get this done in WordPress but a simple way is using is_page like the below.
Alternately if your using another CMS like Craft you can do something like:
Or even make a macro
I hope this helps!
Really nice trick, but when the
.is-active
class is on the last element it doesn’t seem to animate properly. I couldn’t think of any way to fix this, and it might just be my computer…? I tested it using the original pen in this post (link) except with the HTML like this:Still though, love the post :-)
Ta, the issue with the last element not animating correctly is due to the
:last- child
not being a strong enough in specificity vs the sibling selector.One way to fix this is to use
!important
on the selectors. Here is an updated pen with this workingI was hoping to find a way to fix this without having to go to the last resort of
!important
.I’ve asked Chris to tweak the article with the changes :)
You are my new god!
For a vertical example: http://cssmojo.com/menus-with-a-sliding-marker/
Using :hover is the only way this fits within CSS only and in my opinion it’s a bit too bulky for/wasted as a hover effect. It won’t get bang for buck on touch devices and can highlight jankyness that may be present due to an array of reasons.
Regardless, it should be used sparingly if you plan anything more than a simple static site or if the tabs/navs are dynamic and not vertical.
This effect is best used as an animating active tab/nav indicator, using the same technique in conjunction with the .active class of any out of the box tab control. If you know the Navbar won’t change, it’s a very good effect for an SPA or a landing page combined with scrollspy.
A caveat with this technique(if horizontal) is all the list items have to be the same size. This makes it not so straight forward if you plan on creating a dynamic site where tabs could break to another line and kills the translate calcs…this just add complexity you don’t want to maintain.
Polymer has elements which allow variable lengths with tab scrolling but this shows how technical it is to implement yourself in a robust way:
https://elements.polymer-project.org/elements/paper-tabs?view=demo:demo/index.html&active=paper-tabs
100% agree! it is more of a CSS Trick … see what i did there :|
Cool stuff! One thing though: if I hover the indicator (just the red lines, either the upper or the lower one), it jumps all the way to the last child (Opera, Chrome, Firefox, IE11).
Thanks Andrei, I didn’t catch this one but if you make the indicator large it becomes very obvious.
This would be due to the
:hover
on the indicator actually being on part of thelast-child
.Chris has done a video that covers using this behaviour intentionally . As we don’t want this behaviour – if we use pointer-events in this case
pointer-events: none;
on the:before
and:after
that make up the indicator it will stop this happening in the supported browsers.Cool idea to see in practice. Just thought I give feedback on something I found out recently to clean up your SASS slightly.
can be
Doc Reference: http://sass-lang.com/documentation/Sass/Script/Functions.html#percentage-instance_method
Actually you don’t have to do any drama, can achieve easily with: