Skip to main content

Spinner

Dynamic loading indicators with multiple animation styles, sizes, and speeds for various loading states

Spinner

The Spinner component provides animated loading indicators with four distinct animation styles. It dynamically loads SVG animations, offers size and speed control, and includes fallback states for robust loading experiences.

Spinner Types

Four distinct animation styles for different contexts

three-dots
Simple and minimal
<Spinner type="three-dots" />
12-dots
Circular progression
<Spinner type="12-dots" />
gooey-1
Organic movement
<Spinner type="gooey-1" />
gooey-2
Fluid animation
<Spinner type="gooey-2" />

API Reference

Props

PropTypeDefaultDescription
type'three-dots' \| '12-dots' \| 'gooey-1' \| 'gooey-2''three-dots'Animation style
sizenumber24Size in pixels
colorstring'currentColor'Spinner color
speed'slow' \| 'normal' \| 'fast''normal'Animation speed
classstring''Additional CSS classes

Animation Types

TypeDescriptionBest For
three-dotsSimple horizontal dotsMinimal interfaces, inline loading
12-dotsCircular scaling dotsPrimary loading states
gooey-1Organic blob animationPlayful, modern interfaces
gooey-2Fluid morphing animationCreative, distinctive loading

Speed Modifiers

SpeedDuration MultiplierUse Case
slow1.5xCalm, patient interfaces
normal1xStandard loading indication
fast0.75xQuick, responsive feedback

Usage Examples

Basic Spinner

<Spinner />

Custom Type and Size

<Spinner type="12-dots" size={32} />
<Spinner type="gooey-1" size={48} />

Button with Loading State

<script>
let isLoading = $state(false);

async function handleSubmit() {
  isLoading = true;
  await saveData();
  isLoading = false;
}
</script>

<Button onclick={handleSubmit} disabled={isLoading}>
  {#if isLoading}
    <Spinner size={16} color="currentColor" />
    Saving...
  {:else}
    Save Changes
  {/if}
</Button>

Inline Loading Indicator

<p>
  Fetching latest data
  <Spinner size={14} type="three-dots" class="inline-spinner" />
</p>

Full Page Loading

<div class="loading-overlay">
  <Spinner type="gooey-1" size={64} />
  <p>Loading application...</p>
</div>

Input with Search Indicator

<div class="search-input">
  <input type="text" placeholder="Searching..." />
  {#if isSearching}
    <Spinner size={20} class="search-spinner" />
  {/if}
</div>

Design Patterns

1. Loading States

Different spinner types convey different loading experiences:

  • three-dots: Subtle, unobtrusive loading
  • 12-dots: Clear, prominent loading indication
  • gooey: Engaging, modern loading experience

2. Size Guidelines

Choose sizes based on context:

  • 14-16px: Inline text, small buttons
  • 20-24px: Form inputs, regular buttons
  • 32-40px: Cards, sections
  • 48-64px: Full page, modals

3. Color Inheritance

By default, spinners use currentColor, inheriting the text color of their parent. This ensures consistency with your design system:

<!-- Inherits button's text color -->
<Button variant="primary">
  <Spinner size={16} />
</Button>

<!-- Explicit color -->
<Spinner color="var(--color-semantic-info)" />

Performance Considerations

Dynamic Loading

The Spinner component loads SVG files on-demand:

  1. Lazy Loading: SVGs are fetched only when rendered
  2. Caching: Browser caches loaded SVGs automatically
  3. Fallback: Shows inline SVG if external file fails

Animation Performance

  • Uses CSS animations for smooth 60fps performance
  • No JavaScript animation loops
  • GPU-accelerated transforms where possible

Bundle Size

  • Component: ~4.5KB uncompressed
  • SVG files loaded separately (not bundled)
  • Tree-shakeable when not used

Accessibility

Screen Reader Support

<!-- Visible loading text -->
<Spinner size={24} />
<span>Loading content...</span>

<!-- Hidden from screen readers -->
<div aria-busy="true" aria-live="polite">
  <Spinner size={24} />
  <span class="sr-only">Loading...</span>
</div>

ARIA Attributes

The component includes:

  • role="status" for loading indication
  • aria-label="Loading..." for context
  • Works with aria-busy on containers

Focus Management

Spinners don’t receive focus, but consider:

  • Disabling interactive elements during loading
  • Announcing loading completion to screen readers
  • Maintaining focus position after loading

States and Error Handling

Loading States

The component handles three states internally:

  1. Loading: Shows pulse animation while fetching SVG
  2. Success: Displays the animated spinner
  3. Error: Shows fallback inline SVG

Fallback Behavior

<!-- If SVG fails to load, shows built-in fallback -->
<Spinner type="invalid-type" />
<!-- Renders simple animated dots -->

Styling

Custom Classes

<Spinner class="my-spinner" />

<style>
  :global(.my-spinner) {
    opacity: 0.8;
    margin: 0 8px;
  }
</style>

Inline Positioning

<style>
  :global(.inline-spinner) {
    vertical-align: middle;
    margin: 0 4px;
  }
</style>

Best Practices

Do’s

  • ✅ Match spinner size to context (small for buttons, large for pages)
  • ✅ Provide loading text for clarity
  • ✅ Disable interactions during loading
  • ✅ Use consistent spinner types across your app

Don’ts

  • ❌ Don’t use multiple spinner types in the same context
  • ❌ Don’t make spinners too small (< 14px) or too large (> 64px)
  • ❌ Don’t forget to handle loading completion
  • ❌ Don’t use fast speed for long operations

Migration Guide

If migrating from static loading indicators:

<!-- Before: CSS-only spinner -->
<div class="css-spinner"></div>

<!-- After: Spinner component -->
<Spinner type="12-dots" size={24} />

For custom SVG spinners:

<!-- Before: Inline SVG -->
<svg class="spinner">...</svg>

<!-- After: Add to /static/svg/spinners/ and use -->
<Spinner type="your-spinner" />
  • Button: Often contains spinners for loading states
  • Card: May show spinners while content loads
  • Icon: Static icons, while Spinner provides animated ones
  • Skeleton: Alternative loading pattern for content

TypeScript Support

The component is fully typed:

interface Props {
  type?: 'three-dots' | '12-dots' | 'gooey-1' | 'gooey-2';
  size?: number;
  color?: string;
  speed?: 'slow' | 'normal' | 'fast';
  class?: string;
}