Stripe-Like Smooth Transition CSS Only Menu
📣 Sponsor
One of my favourite websites out there is at stripe.com. There's a lot of WebGL, but the menu at the top has this very cool slide effect so it appears to morph into the next option as you move through it. I looked at how it was made, and there was a lot of Javascript going on, but it got me thinking if a similar effect could be done with just CSS.
In this tutorial we'll be covering how I did that, and some of the nuances of how I ended up with a clean solution with smooth animations. This is quite a big menu, so I'd recommend a transition to a hamburger menu for smaller screens.
Layout
Let's talk a bit about the layout. The top level menu is a flexbox, evenly distributed. Within each of those menu items is a sub-menu which we center with flex box's justify-contents
, and then position absolutely.
Each sub-menu is then positioned as a grid, and the layout within each is a grid layout using grid-template
. All widths and sizes are in em
which makes it easy to work with, and also resizable. The basic structure is shown below:
Elements
The only noticeable difference here from a typical menu structure is the inclusion of a div at the very end called #sub-menu-container
.
The reason we have this is that we have to separate the background from the content to create a fluid transition effect. We are using the tilde or ~
operator in our CSS, and since #sub-menu-container
is a sibling of each menu item, we can adjust its position as the user hovers over menu items.
This means that we can transition and animate the movement of the background smoothly, as you can see in the video below:
Since they are separate, for some parts we have to adjust the position of the background, but this is not too time consuming with em
. As such, the manual effort to adjust this effect is kept to a minimum, and could be even less if I'd decided to use SASS or a little bit of Javascript. For the purposes of this tutorial though, I wanted to keep with pure CSS.
Clip Path Rounded Corners
So it's not very well known, but clip-path: inset()
allows for rounded borders. We can do something like clip-path: inset(0 10% 0 0 round 10px)
and the rectangle that is produced will have the equivalent of border-radius: 10px
. For our purposes, this is really useful. We can clip the background layer to animate its movement.
Clip Path Icon Animations
Another subtle effect I am using here is clip-path
on the arrow icons. When you hover over a sub-menu item, an arrow will sometimes appear. Using clip-path, we can fade this in. Subtle, but a nice effect.
Triangles with CSS
This is a pretty old trick, but the arrows used for indicating the active sub-menu are generated with CSS using the border property. The CSS for that looks like this:
.menu-text:after {
opacity: 0;
content: '';
position: absolute;
pointer-events: none;
bottom: -5em;
left: calc(50% - 10px);
border-color: transparent transparent white transparent;
border-width: 10px;
border-style: solid;
}
Forwards.. backwards..
Since we aren't using any fancy masking effects here, and the background is detached from the content, if we aren't careful we may have very obvious transitions that don't make sense, like text transitioning outside the content box. To avoid this we can do two things:
- Time the transitions for things like opacity to limit this.
- Use
clip-path
on the content to animate its disappearance faster than the movement of the background.
So that sounds complicated, but essentially it means we will clip-path
the content in time with the background so that the content disappears and doesn't flow over the "edges" of the background, and we'll use smaller animation times for opacity so that items disappear faster than they are noticeably in odd locations.
For example, when you are not hovered over a menu item, the clip-path
for the menu content is set to essentially make the content invisible:
.sub-menu {
clip-path: inset(0 10em 10em 15em);
}
Then on hover, we can undo that, and bring the content into focus. If we do it just right, with a good enough transition, we get a pretty seemless effect of the menu popping into existence:
.sub-menu {
clip-path: inset(0 10em 10em 15em);
opacity: 0;
transition: all 0.25s ease-out, clip-path 0.15s ease-out;
margin-left: -5em;
}
.sub-menu:hover {
clip-path: inset(0 0 0 0);
opacity: 1;
margin-left: 0;
}
To illustrate the animation we are doing here on the content, take a look at the slowed down version applied to the block below:
We perform similar animations on the background, and we end up with the menu effect you can find in the demo. When combined with other moving elements, it gives the effect of a new content rushing in from the edge.
Animation Order with Just CSS
When a user first hovers over a link, we perform an animation where the background seems to bend inwards. Then when we move to the next link, the animation isn't performed again, but the background merges into the next selection. So what gives, how do we have two animations running on hover?
The secret relies on both animation and transition. When you first hover over a link, we run an animation, but this animation is only performed once:
nav .menu-item:hover ~ #sub-menu-container #sub-menu-holder {
opacity: 1;
right: auto;
animation: clipPath 0.25s ease-out 1 forwards;
transition: clip-path 0.25s ease-out 0s, left 0.15s ease-out 0s, height 0.15s ease-out 0s;
}
@keyframes clipPath {
0% {
opacity: 0;
}
100% {
transform: rotateX(0deg) scale(1);
top: 4.5em;
opacity: 1;
}
}
/* Example of specific clip path hover effect for 3rd item */
nav .menu-item:nth-of-type(3):hover ~ #sub-menu-container #sub-menu-holder {
clip-path: inset(0 15em 0 0 round 10px);
}
Once that initial animation is run, we revert back to transitions. So when the user stays on the menu, the animation has already run, and so CSS transitions the background to the next clip-path. For each menu item, we define that clip-path, to ensure we get the look we're after.
Resources
Thanks for reading. I've put the contents for this tutorial on both CodePen and GitHub, and you can find the links to both below: