DEV Community

Cover image for Add a YouTube-like Page Loading Animation in SvelteKit
Shajid Hasan
Shajid Hasan

Posted on

Add a YouTube-like Page Loading Animation in SvelteKit

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:
GIF demo
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

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
andrewngkf profile image
Andrew Ng

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?

`

    import {beforeNavigate, afterNavigate } from '$app/navigation';

beforeNavigate(() => {
    console.log('Navigation started!');
    navigationState.set('loading');
});

afterNavigate(() => {
    console.log('Navigation ended!');
    navigationState.set('loaded');
});
Enter fullscreen mode Exit fullscreen mode

`

Collapse
 
bandit profile image
James Nisbet

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...

Collapse
 
carlosivanchuk profile image
Carlos Ivanchuk

This is exactly what I was looking for. Thanks!

Collapse
 
shajidhasan profile image
Shajid Hasan

Really glad it helped!

Collapse
 
einlinuus profile image
EinLinuus

Awesome! Exactly what I needed. Thank you!