Experimenting with Smooth page transitions with CSS and JavaScript
Note: There are a few performance and accessibility issues with this technique such as a page not being shown at all. This was written from experimenting with my personal site. I plan on experimenting more and improving the reliability of this code. Proceed with caution.
With this article, I'm going to show you how to use CSS and JavaScript with no frameworks (no jQuery) to make seamless page transitions. This technique could be used on custom WordPress sites, static site generator sites (11ty, Jekyl, Gatsby, etc) on web apps, and just plain HTML sites.
Quick overview
It helps to take a step back and think of how we will do the page transitions before writing code. Here is a simple overview of how the page transitions will work.
- Add a class to the
<body>
tag when page content is loaded. - Set default states of CSS to elements you want animated and add a
transition
property. - Use CSS selectors with the loaded body class and add properties on how you want the page to look after loaded
- Delay going to the next page when internal links are clicked so that the page out transitions can play.
Technical implementation
Add a .loaded
class to the body on page load.
<script>
var callback = function() {
// Handler when the DOM is fully loaded
body.classList.add('loaded');
};
if (
document.readyState === 'complete' ||
(document.readyState !== "loading" && !document.documentElement.doScroll)
) {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
</script>
This will execute the loaded()
function once all of the content in the DOM has loaded.
Setup the default CSS and transitions
Set default states of CSS of where we want the transitions to start. For this example we want the header and content to fade in from the left and we want to delay the content to animate a little bit after the header to give it a nice staging load effect.
.header,
.content {
opacity: 0;
transform: translateX(-40px);
transition: all 1s ease;
}
.content {
transition-delay: 0.5s;
}
opacity: 0;
is giving these elements a 0% opacity so that they will be completly hidden.
translateX(-20px)
is telling these elements we want to move them on the x axis -20px of where the element naturally is.
The transition property is what will occur when we add another class to overwrite the elements CSS properties. all
specifies that we want to use all of the CSS properties that change (opacity and transform in this case). 0.3s
is how long we want the transform to occur and ease
is the timing function that the transition will use.
Setup the loaded CSS
Now we will use the .loaded
body class to overwrite the default state styles and this will be how the page looks when it is loaded and used.
.loaded .header,
.loaded .content {
opacity: 1;
transform: translateX(0);
}
This code is overwriting our first styles and telling the page to set the opacity to 100% visible and move the elements back to their natural position on the x-axis.
Now when the page is loaded, by default the .header
and .content
will have opacity 0 and be -20px on the x-axis. After loaded the elements will transition to what we specified in the .loaded .header
CSS in the amount of time we specified on the first CSS.
Your full CSS should look like
.header,
.content {
opacity: 0;
transform: translateX(-40px);
transition: all 1s ease;
}
.content {
transition-delay: 0.5s;
}
.loaded .header,
.loaded .content {
opacity: 1;
transform: translateX(0);
}
Click the live example below to see what we have so far.
See the Pen Simple page transitions with CSS and JavaScript by rdallaire (@rdallaire) on CodePen.
Transitioning to the next page
If you have this setup you will notice when you leave your current page it becomes blank for the current screen and then does the transition for the next page.
We want to be able to have a transition out of the current page and have it reverse the transitions.
To fix this we will
- Stop the next page from loading
- Tell the browser when a link is clicked, remove the
.loaded
class - Grab the
href
of the link that was clicked - After a set amount of time (enough to play the transitions) load the URL that was clicked
The following code is used to do this. It also includes a check to ignore target="_blank"
links since they open in a new tab.
The setTimeout
500
at the bottom can be adjusted depending on how long your transitions take.
document.addEventListener('click', function (e) {
var goTo = e.target.getAttribute('href') !== null ? e.target.getAttribute('href')
: e.target.parentNode.getAttribute('href');
if ((e.target.matches('a') ||
e.target.parentNode.matches('a')) &&
e.target.getAttribute('target') !== '_blank' && !(/^#/.test(goTo))) {
e.preventDefault();
document.body.classList.remove('loaded');
setTimeout(function(){
window.location = goTo;
},500);
}
}, false);
Full code
Below is the full code. You can inline the CSS and JS if you want or paste it into a seperate file.
<body>
<header class="header">
<h1>This is my header</h1>
</header>
<main class="content">
<p>Some content</p>
</main>
</body>
<style>
.header,
.content {
opacity: 0;
transform: translateX(-40px);
transition: all 1s ease;
}
.content {
transition-delay: 0.5s;
}
.loaded .header,
.loaded .content {
opacity: 1;
transform: translateX(0);
}
</style>
<script>
document.addEventListener('click', function (e) {
var goTo = e.target.getAttribute('href') !== null ? e.target.getAttribute('href')
: e.target.parentNode.getAttribute('href');
if ((e.target.matches('a') ||
e.target.parentNode.matches('a')) &&
e.target.getAttribute('target') !== '_blank' && !(/^#/.test(goTo))) {
e.preventDefault();
document.body.classList.remove('loaded');
setTimeout(function(){
window.location = goTo;
},500);
}
}, false);
var callback = function() {
// Handler when the DOM is fully loaded
body.classList.add('loaded');
};
if (
document.readyState === 'complete' ||
(document.readyState !== 'loading' && !document.documentElement.doScroll)
) {
callback();
} else {
document.addEventListener('DOMContentLoaded', callback);
}
Further considerations
Lazy loading images
If you have an image-heavy page or any other type of embeds, you need to consider lazy loading those assets on scroll so that the DOM can be loaded quickly enough for a smooth experience. For my site, I setup images, videos, and iframe embed's to lazy load when they are scrolled to.
Don't transition headers for smooth experience
On my site I decided to not add transitions to the header and navigation. This gives it a feel that only the content is being refreshed similar to a web app, when in reality it is still a full page load.
Add a class on page change
Instead of reversing the transitions you could experiment with adding a class to the body on page change so that when you go to a different page it has a different effect than entering the page.
Updates
- Adjusted the JS to add a class on load