Save Expand icon

Ron Valstar
front-end developer

The basics of CSS page transitions

With single page applications comes the need for page transitions.
Actually page transitions are possible for non SPA but SPAs just make it more obvious to implement.

The reason to have these transitions is to have explicit visual indication that the page has loaded. When you swap pages instantly your brain sometimes fails to register the change, especially with blinking eyes and a slow connection. So it is genuine UX at that.

The transition itself should be nothing more than an indication; we wouldn't want to bore our users with a long cool animation. It should only last a few hundred milliseconds at most.

What happens

Let's first have a look at what really happens before we change something. Let's say you have an <article> element housing the current content and you click an internal link. This is what happens:

What should happen

In the old days (before SPA) an HTTP request would just unload the page and you'd be staring at a blank screen for a second before the new page was loaded and painted.
Browsers these days are 'smarter' and will not unload the page before a server response. This way you can cancel midflight while staying on the current page.
The downside is that the only indication anything is happening is a tiny spinner inside the tab at the top of the window (depending on your OS and browser of course). And that is for direct HTTP requests only. Your own XHR doesn't show anything (hey, you started it, it's your responsibility).

So that means two UX issues to fix.

The first should be a clear continuous animation for the duration of the load but not so obvious that it obscures the content. Plus it should not be visible immediately; with fast loads showing a spinner for a single frame looks really sloppy.
The second indication should feel short and natural. You could give the transition some sort of order (left/right) or hierarchy (top/bottom) but nothing fancy.

Now our timeline becomes

In practice

When the content has loaded the two articles will exist simultaneously for some time. The two cannot occupy the same space so one of them must be position:absolute;. This creates the problem that the containing element will only take the height of the static element. We also need to animate the height of the containing element to that of the new content.

CSS animation

There are rare situations where you must primarily animate by script (with WebGL for instance). In most cases however a simple CSS animation wil suffice. This also keeps your logic and styling separated.
To use the CSS transition we simply toggle classes on the content elements. This adds up to four classes in total: for each element we set the initial state and the final state.

We'll use the following className naming convention: [name]-[type]-[state] which amounts to the following:

.page-enter { opacity: 0; }
.page-enter-to { opacity: 1; }
.page-leave { opacity: 1; }
.page-leave-to { opacity: 0; }

(and yes: you could combine them for brevity but beware of specificity)

Upon transition page-enter is added and two ticks later page-enter-to (same for leave). The two tick interval is for the browser to settle down; if we were to add the page-enter-to immediately it would seem both classes were set simultaneously and no transition would occur.
When the animation is finished the classes are to be removed.
We can determine the finished state with the transitionend event. This event will bubble up so you can apply transitions to child elements if you want to.

A tricky part is the padding and margin to the parent <section> and the first child in the new content element respectively. Padding onto the <section> is not wanted here because the absolute positioned child element simply ignores it. Less logical is the margin of the first child element of the content. It is so counterintuïtive that there is a name for it and an MDN article. So either don't give that first child a top margin or give that content element somewhat of a top padding so that the first child margin doesn't collapse.

Adding time

If you read the previous code you'll notice the pages are not really loaded, they are created from random lorem-ipsum words and shown immediately (because we were keeping it simple and concentrating on CSS).
But we have to add our XHR indication (or spinner) so for the next example we'll fake the XHR with a random timeout. It's either fast or slow, so you'll notice the effect of a delayed indicator (actually you don't, but that's the point).

In reality this can timeout so you'll have to account for that as well. But that is more on the subject of building a proper router. So you must figure that out yourself.
For now, with the basics out of the way, lets have a look at some easy effects.

Page height and left or right

It is a bit more difficult to see with an <iframe /> but we still haven't done anything about the changing page height.

The previous example does look a bit weird when you go from 'contact' to 'home' in that it still animates from right to left. It would be nice to have it change direction depending on the direction of the menu item.

Other easy effects

The examples above use opacity or translation, which is what is mostly used for transitions. Below are some other things you can try.

box-shadow and color

Not many people realise you can use box-shadow for something else than shadows.

masked elements

One nice thing about the more recent CSS capabilities is masking. This can be done by using a property called clip-path and it's values can of course be animated.

stretching and combining

Another easy way to animate is to stretch the content by setting the transform:scale. You can make it even better if you combine different types of movement. Here we translate the header and scale the content. Also note that the header transition is timing function is a cubic-bezier to create a slight bounce.

zoom

Here is a different use of transform:scale that makes it look as though you're zooming in.

3D rotation

A transition onto transform:rotate3d. Note you have to set perspective onto the parent for this to show actual depth.

conclusion

As you can see you can go far with this before having to resort to JavaScript. Just make sure your transitions are of the same length, if they are not you cannot rely on the transitionEnd event but must use a fixed duration or traverse the stylesheets to calculate the exact duration.