We've all seen it. A colored sliver at the top that indicates a page loading in modern websites. YouTube is an excellent example of this. While there might be libraries to do this with NextJS and other frameworks, I have yet to see one that does it for SvelteKit. But fortunately, it isn't so hard to implement yourself! If you're interested in only the code, here's the GitHub repository.
Introduction
Hello there! Welcome to my first blog post ever for developers(for anyone, actually). If you still haven't got a clear picture of what we're going to do, here's a GIF for you:
You can click here for a demo as well. In SvelteKit, you can have a load
function that will run on the server/client before a page is loaded. While the load function does its job, our page loading bar on top will continue to increase for a nice visual feedback. It's awesome!
How
Before we get down to write some code, let me quickly explain how it'll work. I will assume you have at least some experience with Svelte, Svelte's stores and SvelteKit.
We'll have a PageLoader.svelte
component. It starts animating right from the time it's mounted(which is when a user clicks a link). Its width starts to go from 0 to 70%. And as soon as the next page is done loading, it'll quickly animate to 100%, and then disappear nicely. We'll know when navigation is started and ended with SvelteKit's sveltekit:navigation-start
and sveltekit:navigation-end
window events. We'll keep this navigation state data in a Svelte writable
store so that we can control PageLoader.svelte
component's animation.
That's about it! If this sounds simple enough, go ahead and give this a try!
Setup
You will most likely do this in an existing project of yours. But for the sake of this tutorial, let me scaffold a new SvelteKit project with npm init svelte@next
. I will enable TypeScript for this example. After installing the required packages with npm install
, I'll create some files. Here's the directory structure we'll have after that:
src
└───routes
| | __layout.svelte
| | page-1.svelte
| | page-2.svelte
| | index.svelte
| |
└───components
| | PageLoader.svelte
| |
└───stores
| | navigationState.ts
| |
└───styles
| | global.css
Pretty simple, right? I'll add some styling to the global.css
file. I could do that in the Svelte components, but this is a tiny project with shared styles across the pages. You can refer to the GitHub repository of this tutorial if you want the code.
Now it's time to create the pages. I'll just link the page-1.svelte
and page-2.svelte
using anchor tags inside the index.svelte
. Here's the code for these three:
<!-- routes/index.svelte -->
<div class="container">
<div class="content">
<h1>Page loading progress bar in action with SvelteKit.</h1>
<div class="links">
<a href="/page-1">Page 1</a>
<a href="/page-2">Page 2</a>
</div>
</div>
</div>
<!-- routes/page-1.svelte -->
<div class="container">
<div class="content">
<h1>You're on Page 1.</h1>
<div class="links">
<a href="/">Home</a>
<a href="/page-2">Page 2</a>
</div>
</div>
</div>
<!-- routes/page-2.svelte -->
<div class="container">
<div class="content">
<h1>You're on Page 2.</h1>
<div class="links">
<a href="/">Home</a>
<a href="/page-1">Page 1</a>
</div>
</div>
</div>
Nothing too fancy. Now let's move on to the code that's actually relevant.
Let's do this
We'll go to the routes/__layout.svelte
file. All the pages will be rendered inside this layout file, so we'll have to add a slot so that other pages have a place. We'll also import the global.css
here.
<!-- routes/__layout.svelte -->
<script lang="ts">
import '../styles/global.css';
</script>
<slot></slot>
At this point you can run npm run dev
to see if everything's working properly.
Svelte has a special element called <svelte:window>
. You can easily add window event listeners with this element. Upon inspecting the docs, I've come to know SvelteKit emits sveltekit:navigation-start
and sveltekit-navigation-end
window events. Let's just grab them.
<!-- routes/__layout.svelte -->
<script lang="ts">
import '../styles/global.css';
</script>
<svelte:window
on:sveltekit:navigation-start={() => {
console.log('Navigation started!');
}}
on:sveltekit:navigation-end={() => {
console.log('Navigation ended!');
}}
/>
<slot></slot>
Now open up the browser's console, and see if you get the navigation messages properly. All's good on my side so let's go ahead and create the stores/navigationState.ts
file.
// stores/navigationState.ts
import { writable } from 'svelte/store';
type NavigationState = "loading" | "loaded" | null;
export default writable<NavigationState>(null);
Now we have a way to know the navigation state from anywhere within the app. We haven't edited the event listeners for this yet, but we'll get to that.
Before doing anything else, let's create the components/PageLoader.svelte
component. Let's write the markup first.
<!-- components/PageLoader.svelte -->
<div class="progress-bar">
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style>
.progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0.5rem;
}
.progress-sliver {
width: var(--width);
background-color: #f8485e;
height: 100%;
}
</style>
As you can see, we're passing in a $progress * 100
variable into a CSS custom property with the style attribute. progress
is going to be a Svelte tweened
value.
Initially progress
will be 0, and then as soon as the component is mounted, we'll animate it to 0.7 to go to 70%. We'll also listen for the navigationState
change from the store we created earlier. When the state is 'loaded'
, we'll set the progress to 1 (100%). Here's full code:
<!-- components/PageLoader.svelte -->
<script>
import { onDestroy, onMount } from 'svelte';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
import navigationState from '../stores/navigationState';
const progress = tweened(0, {
duration: 3500,
easing: cubicOut
});
const unsubscribe = navigationState.subscribe((state) => {
if (state === 'loaded') {
progress.set(1, { duration: 1000 });
}
});
onMount(() => {
progress.set(0.7);
});
onDestroy(() => {
unsubscribe();
});
</script>
<div class="progress-bar">
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style>
.progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 0.5rem;
}
.progress-sliver {
width: var(--width);
background-color: #f8485e;
height: 100%;
}
</style>
As you can see, we did not forget to unsubscribe the store when the component is destroyed. Also, notice the duration. When we're animating from 0 to 70%, we'll go a bit slow. When the page is loaded, we'll go to 100% much faster.
Now there's only one thing left to do. Let's go back to routes/__layout.svelte
file. We'll change the event listeners so that they can set the navigationState
when navigation is occurring. We're going to import the PageLoader.svelte
component here, and show it only when navigationState
is equal to 'loading'
. We'll bring in Svelte's fade transition as well to make the PageLoader component fade out once a page is loaded. But remember, we'll have to add some delay to it so that the progress bar reaches 100% before it disappears. Take a look at the code now:
<!-- routes/__layout.svelte -->
<script lang="ts">
import { fade } from 'svelte/transition';
import navigationState from '../stores/navigationState';
import PageLoader from '../components/PageLoader.svelte';
import '../styles/global.css';
</script>
<svelte:window
on:sveltekit:navigation-start={() => {
$navigationState = 'loading';
}}
on:sveltekit:navigation-end={() => {
$navigationState = 'loaded';
}}
/>
{#if $navigationState === 'loading'}
<div out:fade={{ delay: 500 }}>
<PageLoader />
</div>
{/if}
<slot />
That's it, folks!
Your page loading animation is ready now. Restart the dev server and check if it works properly.
Conclusion
For my first post ever, I hope that was okay! Check out the GitHub repository if you still have any doubts. I understand this might not be the best way to do this, but it'll work fine for most cases. Feel free to leave any feedback/comments/questions.
Top comments (5)
In using a later version of svelte, you'd want to do this on your layout,
not the
...
on:sveltekit:navigation-start={() => ... method which appears deprecated?
`
`
Enjoyed this, though had some issues where some of my page loads were dismounting the loader well before it finished (not sure why?), which was kind of clunky.
I did this instead, where the loader never dismounts: svelte.dev/repl/e6a86cc325d44b72a9...
Awesome! Exactly what I needed. Thank you!
Really glad it helped!