Enhance User Experience in SvelteKit: Avoid Misdirection with Effective Loading Feedback

on April 18, 2024

Navigating a single-page application, such as those created with SvelteKit, can be confusing due to the lack of loading feedback. However, there's a simple trick to address this issue.

Why might this lack of feedback be confusing to users?

When you are navigating on a regular website (e.g. the ones with lots of index.php files), you always see some spinning or other loading animation inside the browser tab.

You know, that the browser is doing something, so you wait for it.

Let’s say you want to click on a link, which would take you to another place on the website, but you missclick: You realize, that you didn’t hit the link because there is no loading indicator in the page’s tab.

Another scenario would be a page which takes a little bit longer to load, e.g. because it has to fetch some data first.

Even though, the page didn’t change yet, you know something is happening; so you patiently wait (unless it takes way too long).

Now take that same scenario and put it inside a simple Single-Page-Application. Let’s look at the following interactive example:

Try to navigate between the pages ‘First’ and ‘Second’.

First

You click the link, but it takes some time to load. You might have been unsure if this example was broken or if it was interactive at all.

Let’s fix it in SvelteKit

SvelteKit gives you all the necessary tools to quickly counter this issue.

Using the navigating store, which you can import from $app/stores, allows you to subscribe to navigation changes.

The subscribtion triggers on different states, with two being most important to us:

  • navigation started
  • navigation finished
<script>
	import { navigating } from '@app/stores';
	import { onMount } from 'svelte';

	let loading = false;

	onMount(() => {
		navigating.subscribe((value) => {
			// value could be null
			if (!value) return;

			loading = true;
			if (value?.complete) {
				value.complete.then(() => {
					loading = false;
				});
			}
		});
	});
</script>

{#if loading}
	<p>Loading...</p>
{/if}
<!-- Site content -->

First

Making it (somewhat) pretty

While functional, the visual design of the above example is quite basic. So let’s pimp it by transforming it into a loading bar at the top of the page.

First we create two <div> elements, one for the loading-bar container and one for the loading-bar itself.

<div class="loading-bar-container">
	<div class="loading-bar" />
</div>

Now we give it some styling. The container itself is just 2 pixels tall but takes the whole page width. We also need to make sure to always have it in front, so it doesn’t get hidden behind some navigation-bar or some modal. pointer-events: none just makes sure, that we can click through the bar, in case some interactive element gets hidden behind the loading bar.

The loading bar will transition through several states, such as:

  • waiting
  • loading
  • doneLoading
  • cleanupLoading

which will get swapped depending on the current loading state.

.loading-bar-container {
	z-index: 99999;
	pointer-events: none;
	position: fixed;
	top: 0;
	left: 0;
	min-width: 100vw;
	width: 100%;
	height: 2px;
}

.loading-bar {
	display: block;
	height: 100%;
	background-color: #f96743;
}

.loading-bar.waiting {
	width: 0;
	opacity: 0;
}

.loading-bar.loading {
	transition: width cubic-bezier(0, 0.55, 0.45, 1) 20s;
	width: 50%;
	opacity: 1;
}

.loading-bar.doneLoading {
	transition: width ease-in-out var(--duration);
	width: 100%;
	opacity: 1;
}

.loading-bar.cleanupLoading {
	transition: opacity ease-in-out var(--duration);
	opacity: 0;
	width: 100%;
}

With the two elements and their styling in place, we can move on to the logic. Let’s wire it all together.

Like in the very simple example before, we subscribe to changes in navigation. But this time, we do not wait for complete to resolve.

Instead, we keep track of the state ourselves. With the state, we update the class of our loading bar element.

<script>
	import { navigating } from '$app/stores';
	import { onMount } from 'svelte';

	// Duration between states in milliseconds
	const animationDuration = 250;

	let currentState = 'waiting';
	let resetTimeout = undefined;

	onMount(() => {
		navigating.subscribe((n) => {
			if (n) {
				currentState = 'loading';
			} else {
				if (currentState == 'loading') {
					currentState = 'doneLoading';

					if (resetTimeout) {
						clearTimeout(resetTimeout);
						resetTimeout = undefined;
					}

					// Start cleanup animation
					resetTimeout = window.setTimeout(() => {
						currentState = 'cleanupLoading';
						resetTimeout = window.setTimeout(() => {
							currentState = 'waiting';
						}, animationDuration);
					}, animationDuration);
				}
			}
		});
	});
</script>

Finally, we just have to update the loading bar element to change depending on the state.

<div class="loading-bar-container">
	<div class="loading-bar {currentState}" style="--duration: {animationDuration}ms" />
</div>

Demo

First

Now it’s your turn

You can now implement it yourself into your +layout.svelte file.

If you just want a plug’n’play solution, i implemented all of this in a small package, which is also available on npm.

NPM.js

Website

Github Repo

Final words

This is my first post on Svelte topics, and I’m eager to learn and improve. Please share your thoughts and feedback in the comments section below!