Skip to main content

The Problem

Unresponsive interfaces feel broken. When users take action and see nothing happen, they assume failure and retry, creating duplicate requests and frustration.

Perceived Slowness

Without feedback, even fast operations feel slow.

Layout Shift

Content that appears suddenly causes jarring jumps.

Uncertainty

Users don't know if the system is working or frozen.

Retry Storms

Impatient users click repeatedly, overloading systems.

Skeleton Screens

Skeleton screens show the structure of content before it loads, reducing perceived wait time and preventing layout shift.

Text Skeleton

Placeholder for text content. Width varies to suggest natural text rhythm.

"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  import { Skeleton } from '@create-something/components';
"token attr-name">class="token tag"></script>

<!-- Text paragraph skeleton -->
"token attr-name">class="token tag"><div "token attr-name">class="skeleton-group">
  "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="100%" />
  "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="85%" />
  "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="92%" />
  "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="60%" />
"token attr-name">class="token tag"></div>

"token attr-name">class="token tag"><style>
  .skeleton-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }
"token attr-name">class="token tag"></style>

Card Skeleton

Composite skeleton matching a card's structure.

<!-- Card skeleton matching PaperCard structure -->
"token attr-name">class="token tag"><div "token attr-name">class="card-skeleton">
  "token attr-name">class="token tag"><Skeleton "token attr-name">variant="rectangular" "token attr-name">height="160px" />
  "token attr-name">class="token tag"><div "token attr-name">class="card-skeleton-content">
    "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="70%" />
    "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="100%" />
    "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="40%" />
  "token attr-name">class="token tag"></div>
"token attr-name">class="token tag"></div>

"token attr-name">class="token tag"><style>
  .card-skeleton {
    background: var(--color-bg-surface);
    border-radius: var(--radius-lg);
    overflow: hidden;
  }

  .card-skeleton-content {
    padding: var(--space-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
  }
"token attr-name">class="token tag"></style>

Avatar and Circular

Circular skeletons for avatars and icons.

"token attr-name">class="token tag"><Skeleton "token attr-name">variant="circular" "token attr-name">width="32px" "token attr-name">height="32px" />
"token attr-name">class="token tag"><Skeleton "token attr-name">variant="circular" "token attr-name">width="40px" "token attr-name">height="40px" />
"token attr-name">class="token tag"><Skeleton "token attr-name">variant="circular" "token attr-name">width="56px" "token attr-name">height="56px" />

List Skeleton

Multiple items with avatar and text for list views.

"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  import { LoadingSkeleton } from '@create-something/components';
"token attr-name">class="token tag"></script>

<!-- Pre-composed list skeleton pattern -->
"token attr-name">class="token tag"><LoadingSkeleton "token attr-name">variant="list" "token attr-name">count={3} />

<!-- Or compose manually -->
{#each Array(3) as _}
  "token attr-name">class="token tag"><div "token attr-name">class="list-item">
    "token attr-name">class="token tag"><Skeleton "token attr-name">variant="circular" />
    "token attr-name">class="token tag"><div "token attr-name">class="list-item-text">
      "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="50%" />
      "token attr-name">class="token tag"><Skeleton "token attr-name">variant="text" "token attr-name">width="80%" />
    "token attr-name">class="token tag"></div>
  "token attr-name">class="token tag"></div>
{/each}

Spinners

Spinners indicate indeterminate loading when the duration is unknown. Use sparingly, as skeleton screens are generally preferred.

Size Variants

Four sizes for different contexts: inline (sm), button (md), section (lg), page (xl).

Small (16px)
Medium (24px)
Large (32px)
XL (48px)
"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  import { Spinner } from '@create-something/components';
"token attr-name">class="token tag"></script>

"token attr-name">class="token tag"><Spinner "token attr-name">size="sm" "token attr-name">label="Loading..." />
"token attr-name">class="token tag"><Spinner "token attr-name">size="md" "token attr-name">label="Loading..." />
"token attr-name">class="token tag"><Spinner "token attr-name">size="lg" "token attr-name">label="Loading..." />
"token attr-name">class="token tag"><Spinner "token attr-name">size="xl" "token attr-name">label="Loading..." />

Button Loading State

Replace button text with spinner during submission.

"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  let loading = $state(false);

  async function handleSubmit() {
    loading = true;
    try {
      await submitForm();
    } finally {
      loading = false;
    }
  }
"token attr-name">class="token tag"></script>

"token attr-name">class="token tag"><button "token attr-name">onclick={handleSubmit} "token attr-name">disabled={loading}>
  {#if loading}
    "token attr-name">class="token tag"><Spinner "token attr-name">size="sm" />
    "token attr-name">class="token tag"><span>Submitting..."token attr-name">class="token tag"></span>
  {:else}
    Submit
  {/if}
"token attr-name">class="token tag"></button>

Centered Spinner

Full-width centered spinner for section or page loading.

"token attr-name">class="token tag"><Spinner "token attr-name">size="lg" centered "token attr-name">label="Loading content..." />

Progress Indicators

Progress bars show determinate progress when the completion percentage is known.

Linear Progress

Horizontal bar showing percentage complete.

Uploading files... 45%
"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  import { Progress } from '@create-something/components';

  let progress = $state(45);
"token attr-name">class="token tag"></script>

"token attr-name">class="token tag"><Progress
  "token attr-name">value={progress}
  "token attr-name">max={100}
  "token attr-name">label="Uploading files..."
  showValue
/>

Indeterminate Progress

Animated bar for operations with unknown duration.

"token attr-name">class="token tag"><Progress indeterminate "token attr-name">label="Processing..." />

Step Progress

For multi-step processes like forms or wizards.

Step 2 of 4
"token attr-name">class="token tag"><Progress
  "token attr-name">value={2}
  "token attr-name">max={4}
  "token attr-name">label="Step 2 of 4"
  "token attr-name">variant="steps"
/>

Loading Overlay

Overlay patterns block interaction during critical operations.

Full Page Overlay

Blocks entire page during authentication or initial data load.

Page content appears here...

Loading application...

"token attr-name">class="token tag"><script "token attr-name">lang="ts">
  import { LoadingOverlay } from '@create-something/components';

  let loading = $state(true);
"token attr-name">class="token tag"></script>

{#if loading}
  "token attr-name">class="token tag"><LoadingOverlay "token attr-name">message="Loading application..." />
{/if}

"token attr-name">class="token tag"><main>
  <!-- Page content -->
"token attr-name">class="token tag"></main>

Section Overlay

Blocks only a portion of the page while content loads.

"token attr-name">class="token tag"><div "token attr-name">class="section-container">
  "token attr-name">class="token tag"><LoadingOverlay
    "token attr-name">variant="section"
    "token attr-name">visible={loading}
  />
  "token attr-name">class="token tag"><div "token attr-name">class="section-content">
    {#if data}
      <!-- Render data -->
    {/if}
  "token attr-name">class="token tag"></div>
"token attr-name">class="token tag"></div>

Choosing a Pattern

Different loading states require different feedback patterns.

Skeleton Screens

When:

  • Loading page content or lists
  • Layout structure is known in advance
  • Duration is typically under 3 seconds

Reduces perceived wait time by showing structure immediately.

Spinners

When:

  • Button or inline loading states
  • Brief operations (under 2 seconds)
  • Unknown or variable layout

Simple feedback for short, contained operations.

Progress Bars

When:

  • File uploads or downloads
  • Multi-step processes
  • Progress percentage is calculable

Shows actual progress, sets expectations for wait time.

Overlays

When:

  • Critical operations that must complete
  • Preventing user interaction is necessary
  • Authentication or checkout flows

Prevents conflicting actions during important operations.

Reduced Motion

All loading animations must respect prefers-reduced-motion.

Motion Alternatives

When motion is reduced, provide static alternatives that still communicate loading.

/* Spinner - stops animation, shows static indicator */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: none;
    border-color: var(--color-fg-muted);
    border-top-color: var(--color-fg-primary);
  }
}

/* Skeleton - stops pulse, shows static gray */
@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
    opacity: 0.7;
  }
}

/* Progress indeterminate - shows static bar */
@media (prefers-reduced-motion: reduce) {
  .progress-indeterminate .progress-fill {
    animation: none;
    width: 30%;
    left: 35%;
  }
}

Token Reference

Loading patterns use these Canon design tokens.

--color-bg-subtle Skeleton background color (#1a1a1a)
--color-border-default Spinner track color
--color-fg-primary Spinner accent, progress fill
--color-overlay Overlay backdrop (rgba(0,0,0,0.5))
--radius-sm / --radius-md Skeleton border radius
--radius-full Spinner and circular skeleton
--z-modal Loading overlay z-index (100)

Anti-Patterns

Common loading mistakes to avoid.

Spinner for Everything

Don't use spinners when you can show structure. Skeleton screens reduce perceived wait time more effectively.

No Loading State

Any operation over 100ms needs feedback. Users assume broken UI without response.

Layout Shift

Match skeleton dimensions to final content. Jumping layouts frustrate users.

Ignoring Reduced Motion

Animated loaders can cause discomfort. Always provide static alternatives.

Overlong Spinners

If loading takes more than 10 seconds, show progress or allow cancellation.

Blocking Non-Critical

Don't overlay the entire page for a single component. Load progressively.