Add a YouTube-like Page Loading Animation in SvelteKit
Tags: svelte, webdev
Published: 2021-08-20 09:11:55

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](https://github.com/shajidhasan/sveltekit-page-progress-demo).

## 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:
![GIF demo](https://github.com/shajidhasan/sveltekit-page-progress-demo/raw/main/demo/screen-cap.gif)
You can [click here](https://sveltekit-progress.vercel.app) 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:
```html
<!-- 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>
```

```html
<!-- 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>
```

```html
<!-- 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.
```html
<!-- 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.
```html
<!-- 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.
```typescript
// 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.
```html
<!-- 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:
```html
<!-- 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:
```html
<!-- 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](https://github.com/shajidhasan/sveltekit-page-progress-demo) 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.