DEV Community

Cover image for Svelte app: create Loading Overlay component using svelte/motion
Alexey Pavlov
Alexey Pavlov

Posted on

Svelte app: create Loading Overlay component using svelte/motion

For one of my projects, I needed to create a loading overlay component to hide the app until the loading is complete. In this article, I want to share how I did it.

First, let's sketch out the HTML and CSS markup. This is quite simple. I create a block, position it fixed, stretch it to full-screen size, and centre its content. Notice how we use the svg gradient to fill the text with different colours.

<div class="overlay">
    <svg width="115" height="32" viewBox="0 0 78 32" xmlns="http://www.w3.org/2000/svg">
        <defs>
            <linearGradient id="filler">
                <stop offset="50%" stop-color="#ff006e" />
                <stop offset="50%" stop-color="white" />
            </linearGradient>
        </defs>

        <text x="0" y="24px" fill="url(#filler)">Loading...</text>
    </svg>
</div>

<style>
  .overlay {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 10001;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #3a86ff;
    font-family: "Arial", sans-serif;
    font-size: 1.375rem;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Cool. We are done with the markup. Now let's move on to animation. To do this, we will use standard Svelte tools. Using tweened from svelte/motion we create a progress variable and use its value in the svg gradient.

const progress = tweened(0, { easing: linear, duration: 3000 });
Enter fullscreen mode Exit fullscreen mode
  <linearGradient id="filler">
    <stop offset={`${$progress}%`} stop-color="#ff006e" />
    <stop offset={`${$progress}%`} stop-color="white" />
  </linearGradient>
Enter fullscreen mode Exit fullscreen mode

Next, let's create animate function that we will call when the component mounts. We make this function recursive so that the animation continues indefinitely. Like any other recursive function, it must have an exit point. Therefore, in order to avoid memory leaks, we make sure that the execution of the function ends when the component instance is destroyed.

  let destroyed = false

  async function animate() {
    if (destroyed) {
      return
    }

    await width.set(100)
    await width.set(0, { duration: 0 })
    await animate()
  }

  onMount(() => {
    animate()
  })

  onDestroy(() => {
    destroyed = true
  })
Enter fullscreen mode Exit fullscreen mode

Fine. Our component already looks good. Now we will improve the resulting animation by making it two-stage, where in the first stage we paint over the white text with pink, and vice versa, in stage number 2 we paint over the pink text with white. To do this, we will create a writable store with a boolean value that we will use to indicate the current stage of the animation. With each call of the animate function, we will change this value to the opposite.

  const isFirstStage = writable(true)

  async function animate() {
    if (destroyed) {
      return
    }

    await width.set(100)
    await width.set(0, { duration: 0 })
    isFirstStage.update((value) => !value)
    await animate()
  }
Enter fullscreen mode Exit fullscreen mode
<linearGradient id="filler">
  <stop offset={`${$progress}%`} stop-color={$isFirstStage ? '#ff006e' : 'white'} />
  <stop offset={`${$progress}%`} stop-color={$isFirstStage ? 'white' : '#ff006e'} />
</linearGradient>
Enter fullscreen mode Exit fullscreen mode

So, we are done with the loading indicator component itself. Now let's imagine that there can be several components that use our indicator. We could render our overlay in each of them, but let's better refine our approach and create functionality that will allow us to render the indicator once and provide functions for turning the indicator on and off. To do this, we will create another writable store, which will contain requests to display the indicator. The indicator itself will be rendered as long as there is at least one request left in the store.

import { writable, derived } from "svelte/store";
import { nanoid } from "nanoid";

export const loadingOverlayQueue = writable([]);

export function showLoadingOverlay() {
  const newRequestId = nanoid();

  loadingOverlayQueue.update((currentValue) => [...currentValue, newRequestId]);

  return newRequestId;
}

export function hideLoadingOverlay(requestId) {
  loadingOverlayQueue.update((currentValue) =>
    currentValue.filter((item) => item !== requestId)
  );
}

export const isLoadingOverlayShown = derived(
  loadingOverlayQueue,
  (requests) => requests.length > 0
);
Enter fullscreen mode Exit fullscreen mode

Now, all done. We've created an original animated loading indicator in a few easy steps. The source code can be found on my GitHub. Do you enjoy working with svelte?

Top comments (1)

Collapse
 
pskate profile image
Ekaterina Pavlova

Looks cool! Thanks for tutorial 👍