Svelte app: create Loading Overlay component using svelte/motion
Tags: svelte, javascript, tutorial, frontend
Published: 2022-05-26 06:26:05

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.

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

{% codesandbox 1ypl81 %}

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`](https://svelte.dev/docs#run-time-svelte-motion-tweened) from `svelte/motion` we create a `progress` variable and use its value in the svg gradient.

```js
const progress = tweened(0, { easing: linear, duration: 3000 });
```

```html
  <linearGradient id="filler">
    <stop offset={`${$progress}%`} stop-color="#ff006e" />
    <stop offset={`${$progress}%`} stop-color="white" />
  </linearGradient>
```

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.

```js
  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
  })
```

{% codesandbox fg3881 %}

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.

```js
  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()
  }
```

```html
<linearGradient id="filler">
  <stop offset={`${$progress}%`} stop-color={$isFirstStage ? '#ff006e' : 'white'} />
  <stop offset={`${$progress}%`} stop-color={$isFirstStage ? 'white' : '#ff006e'} />
</linearGradient>
```

{% codesandbox tpk15e %}

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.

```js
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
);
```

{% codesandbox 8zvtuu %}

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](https://github.com/alpavlove/svelte-loading-overlay). Do you enjoy working with svelte?