Continuous Top Loading Bar
tl;dr The implementation is published here: https://github.com/nag5000/continuous-top-loading-bar
Some time ago, I needed to add a simple loading animation between page transitions, similar to how it is done on YouTube and GitHub:
I tried several existing libraries, and most of them provide an API to set the current loading progress (like setProgress(0), setProgress(0.5), setProgress(1)
), but in the case of page transitions, this wasn't quite what I needed. The transition time is unknown, and generally, there's no way to measure the progress when considering various pages whose loading processes are encapsulated within them. The most suitable solution was to show an exponentially decelerating loading progress that completes when the new page appears. The initial speed can be adjusted experimentally to match the average page loading speed on the site. Therefore, I was looking for a solution with a decaying animation that would be simple to use with two methods: start()
and done()
.
I found a couple of such libraries, but they didn't suit me for various reasons:
- not lightweight (because the implementation considered manual progress setting and some edge cases),
- not customizable enough,
- I couldn't achieve an exponentially decelerating animation I wanted,
so I decided to write my own implementation (tl;dr https://github.com/nag5000/continuous-top-loading-bar).
Since I didn't need the ability to manually set the loading progress but rather just the ability to start and finish a predefined animation, I considered options with CSS animations. The most suitable option turned out to be the Web Animations API, which allows creating animations based on CSS properties and starting them programmatically:
const keyframes = new KeyframeEffect(...);
const an = new Animation(keyframes)
an.play()
// an.finish()
// an.pause()
// an.cancel()
I defined the progress bar element in CSS as follows:
position: "fixed";
top: 0;
left: 0;
width: 100%;
height: 4px;
backgroundColor: "violet";
transform: translate(-100%, 0);
opacity: 0;
pointer-events: none;
So to increase the progress from 0 to 100%, I would animate the transform: translate(-100%, 0)
property to transform: translate(0, 0)
.
Next, I needed to define the keyframes to create the desired exponentially decelerating animation. I experimented with some math and different approaches and eventually settled on the following:
let timeIntervalsMs = [0, 500, 2500, 500, 2500, 10_000]
let dur = 0;
let offsets = timeIntervalsMs.map((x) => { dur += x; return dur; }).map((x) => x / dur)
console.log(dur, offsets)
// -> 16000, [0, 0.03125, 0.1875, 0.21875, 0.375, 1]
function getProgressKeyframes(bar: HTMLElement): KeyframeEffect {
return new KeyframeEffect(
bar,
[
{ opacity: 1, offset: 0 },
{ transform: "translate(-70%, 0)", offset: 0.03125 },
{ transform: "translate(-60%, 0)", offset: 0.1875 },
{ transform: "translate(-30%, 0)", offset: 0.21875 },
{ transform: "translate(-20%, 0)", offset: 0.375 },
{ opacity: 1, transform: "translate(-4%, 0)", offset: 1 },
],
{
duration: 16000,
fill: "forwards",
},
);
}
The first segment of the animation lasts 500ms, the second one goes slowly for 2500ms, the third one speeds up again for 500ms, the fourth one slows down for 2500ms, and the fifth one drags on for a long time for 10000ms. The animation lasts 16 seconds in total, and the loading progress changes from 0 to 1. It didn't turn out to be an exponential deceleration, but I liked the result.
To end the animation, I added a separate animation that brings the progress to 100% and smoothly hides the loading bar:
function getDoneKeyframes(bar: HTMLElement): KeyframeEffect {
return new KeyframeEffect(
bar,
[
{ opacity: 1, offset: 0 },
{ opacity: 1, offset: 0.4 },
{ transform: "translate(0%, 0)", offset: 0.5 },
{ opacity: 0, transform: "translate(0%, 0)", offset: 1 },
],
{
duration: 1000,
easing: "ease",
fill: "forwards",
},
);
}
The implementation turned out to be quite simple and lightweight.
You can see the full code in the repository: https://github.com/nag5000/continuous-top-loading-bar/blob/main/index.ts
The package is published on npm as continuous-top-loading-bar
.
You can also play with it here: https://codesandbox.io/p/sandbox/continuous-top-loading-bar-gz7g4l?file=%2Findex.html