Multi Step Loader

A step loader for screens that take a lot of time to load.

Installation

Install Dependencies

npm i svelte-motion clsx tailwind-merge @tabler/icons-svelte

Add util file

src/lib/utils/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
        
export function cn(...inputs: ClassValue[]) {
    return twMerge(clsx(inputs));
}

Add the following code in tailwind.config.ts file

tailwind.config.ts
import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette';
// other imports...

const config = {
	// ... other properties
	content: ['./src/**/*.{ts,tsx}'],
	darkMode: 'class',
	theme: {
		// rest of theme code
	},
	plugins: [addVariablesForColors]
};

function addVariablesForColors({ addBase, theme }: any) {
	let allColors = flattenColorPalette(theme('colors'));
	let newVars = Object.fromEntries(
		Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
	);

	addBase({
		':root': newVars
	});
}

Copy the source code

src/lib/components/ui/MultiStepLoader/MultiStepLoader.svelte
<script lang="ts">
	import { AnimatePresence, Motion } from 'svelte-motion';
	import LoaderCore from './LoaderCore.svelte';

	type LoadingState = {
		text: string;
	};

	export let loadingStates: LoadingState[];
	export let loading: boolean | undefined = undefined;
	export let duration: number = 2000;
	export let loop: boolean = true;

	let currentState = 0;

	function updateState() {
		if (!loading) {
			currentState = 0;
			return;
		}
		const timeout = setTimeout(() => {
			currentState = loop
				? currentState === loadingStates.length - 1
					? 0
					: currentState + 1
				: Math.min(currentState + 1, loadingStates.length - 1);
		}, duration);

		return () => clearTimeout(timeout);
	}

	$: currentState || loading, updateState();
</script>

<AnimatePresence show={true}>
	{#if loading}
		<Motion
			let:motion
			initial={{
				opacity: 0
			}}
			animate={{
				opacity: 1
			}}
			exit={{
				opacity: 0
			}}
		>
			<div
				use:motion
				class="fixed inset-0 z-[100] flex h-full w-full items-center justify-center backdrop-blur-2xl"
			>
				<div class="relative h-96">
					<LoaderCore value={currentState} {loadingStates} />
				</div>

				<div
					class="absolute inset-x-0 bottom-0 z-20 h-full bg-white bg-gradient-to-t [mask-image:radial-gradient(900px_at_center,transparent_30%,white)] dark:bg-black"
				/>
			</div>
		</Motion>
	{/if}
</AnimatePresence>
src/lib/components/ui/MultiStepLoader/LoaderCore.svelte
<script lang="ts">
	import { cn } from '@/utils';
	import { Motion } from 'svelte-motion';

	type LoadingState = {
		text: string;
	};

	export let loadingStates: LoadingState[];
	export let value: number = 0;
</script>

<div class="relative mx-auto mt-40 flex max-w-xl flex-col justify-start">
	{#each loadingStates as loadingState, index (index)}
		{@const distance = Math.abs(index - value)}
		<!-- Minimum opacity is 0, keep it 0.2 if you're sane. -->
		{@const opacity = Math.max(1 - distance * 0.2, 0)}

		<Motion
			let:motion
			initial={{ opacity: 0, y: -(value * 40) }}
			animate={{ opacity: opacity, y: -(value * 40) }}
			transition={{ duration: 0.5 }}
		>
			<div use:motion class={cn('mb-4 flex gap-2 text-left')}>
				<div>
					{#if index > value}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
							stroke-width={1.5}
							stroke="currentColor"
							class={cn('h-6 w-6 text-black dark:text-white')}
						>
							<path d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
						</svg>
					{/if}

					{#if index <= value}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 24 24"
							fill="currentColor"
							class={cn(
								'h-6 w-6 ',
								cn(
									'text-black dark:text-white',
									value === index && 'text-black opacity-100 dark:text-lime-500'
								)
							)}
						>
							<path
								fill-rule="evenodd"
								d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
								clip-rule="evenodd"
							/>
						</svg>
					{/if}
				</div>
				<span
					class={cn(
						'text-black dark:text-white',
						value === index && 'text-black opacity-100 dark:text-lime-500'
					)}
				>
					{loadingState.text}
				</span>
			</div>
		</Motion>
	{/each}
</div>
src/lib/components/ui/MultiStepLoader/index.ts
import MultiStepLoader from './MultiStepLoader.svelte';
import LoaderCore from './LoaderCore.svelte';

export { MultiStepLoader, LoaderCore };

Props

MultiStepLoader.svelte
Prop Type Description
loadingStates {text: string}[] An array of objects, each with a text property displaying the current load state message.
loading boolean | undefined A boolean to control whether the loader is active. Default: undefined.
duration number The milliseconds between transitioning to the next loading state. Default: 2000.
loop boolean A boolean to determine whether the loading state will loop. Default: true.
LoaderCore.svelte
Prop Type Description
value number | undefined The current index of the loading state eing displayed. Default: 0.