Page content appears here...
Loading
Loading patterns communicate system status during asynchronous operations. Skeleton screens, spinners, and progress indicators keep users informed and reduce perceived wait time.
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).
"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.
"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.
"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.
"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.