Spinner

spinner.svelte

A lightweight single-cell rotating ASCII / braille loader with preset patterns.

Demo

Scanning modules... | Connecting... Syncing... Processing... [= ] Building...
Usage
001 <script lang="ts">
002 import Spinner from '$lib/components/spinner.svelte';
003 </script>
004
005 <div class="flex flex-wrap items-center gap-6">
006 <Spinner variant="dots" label="Scanning modules..." />
007 <Spinner variant="line" label="Connecting..." />
008 <Spinner variant="arc" label="Syncing..." />
009 <Spinner variant="pulse" label="Processing..." />
010 <Spinner variant="bar" label="Building..." />
011 </div>
012
spinner.svelte
001 <script lang="ts" module>
002 import type { HTMLAttributes } from 'svelte/elements';
003
004 export type SpinnerVariant = 'dots' | 'line' | 'arc' | 'pulse' | 'bar';
005
006 export type SpinnerProps = HTMLAttributes<HTMLSpanElement> & {
007 ref?: HTMLSpanElement | null;
008 variant?: SpinnerVariant;
009 frames?: string[];
010 interval?: number;
011 label?: string;
012 };
013
014 const SPINNER_PRESETS: Record<SpinnerVariant, string[]> = {
015 dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
016 line: ['|', '/', '-', '\\'],
017 arc: ['◜', '◠', '◝', '◞', '◡', '◟'],
018 pulse: ['█', '▓', '▒', '░', '▒', '▓'],
019 bar: ['[= ]', '[== ]', '[=== ]', '[ ===]', '[ ==]', '[ =]', '[ ]']
020 };
021 </script>
022
023 <script lang="ts">
024 let {
025 ref = $bindable(null),
026 variant = 'dots',
027 frames,
028 interval = 80,
029 label,
030 class: className = '',
031 ...restProps
032 }: SpinnerProps = $props();
033
034 const activeFrames = $derived(frames ?? SPINNER_PRESETS[variant] ?? SPINNER_PRESETS.dots);
035 let frameIndex = $state(0);
036
037 $effect(() => {
038 const total = activeFrames.length;
039 if (total <= 1) return;
040
041 const timer = setInterval(() => {
042 frameIndex = (frameIndex + 1) % total;
043 }, interval);
044
045 return () => clearInterval(timer);
046 });
047 </script>
048
049 <span bind:this={ref} class="spinner-wrap {className}" {...restProps} role="status">
050 <span class="spinner-glyph">{activeFrames[frameIndex] ?? activeFrames[0]}</span>
051 {#if label}
052 <span class="spinner-label">{label}</span>
053 {/if}
054 </span>
055
056 <style>
057 .spinner-wrap {
058 display: inline-flex;
059 align-items: center;
060 gap: 1ch;
061 font-family: var(--font-family-mono);
062 font-size: var(--font-size);
063 line-height: var(--line);
064 height: var(--line);
065 color: var(--text-primary);
066 vertical-align: baseline;
067 user-select: none;
068 }
069
070 .spinner-glyph {
071 display: inline-block;
072 color: var(--focus-ring);
073 font-family: inherit;
074 text-align: center;
075 min-width: 1ch;
076 }
077
078 .spinner-label {
079 color: inherit;
080 }
081 </style>
082