Skeleton

Skeleton loading patterns to display placeholder content while data is being fetched. Reduce perceived load time with animated placeholders.

Overview

Skeleton loaders create a visual preview of content that will appear, giving users an immediate sense of the page structure. They are preferable to spinners for content-heavy pages as they maintain layout stability and feel faster.

Key features:

  • Pure CSS animation using @keyframes for smooth pulse effect
  • Accessible loading states with aria-busy="true"
  • Flexible placeholder sizes matching your content dimensions
  • Uses Vanilla Breeze CSS custom properties for consistent theming
  • Layout primitives like data-layout="stack" for structure

Text Placeholder

Display placeholder lines for text content like paragraphs. Use varying widths to simulate natural text flow, with the last line typically shorter.

<layout-card aria-busy="true" aria-label="Loading content"> <div data-layout="stack" data-layout-gap="s"> <div class="skeleton skeleton-heading"></div> <div class="skeleton skeleton-line"></div> <div class="skeleton skeleton-line"></div> <div class="skeleton skeleton-line"></div> </div> </layout-card>

Card Placeholder

A complete card skeleton with image placeholder, heading, text lines, and button. Use this pattern for product cards, blog posts, or any content card that loads asynchronously.

<layout-card class="card-skeleton" aria-busy="true" aria-label="Loading card"> <div class="skeleton skeleton-image"></div> <div class="card-content" data-layout="stack" data-layout-gap="m"> <div data-layout="stack" data-layout-gap="s"> <div class="skeleton skeleton-heading"></div> <div class="skeleton skeleton-line full"></div> <div class="skeleton skeleton-line medium"></div> <div class="skeleton skeleton-line short"></div> </div> <div class="skeleton skeleton-button"></div> </div> </layout-card>

Table Placeholder

Table skeleton with real headers and placeholder rows. Includes avatar, checkbox, and various cell widths to match typical table layouts. Use this when loading tabular data.

<layout-card data-padding="none" aria-busy="true" aria-label="Loading table data"> <table class="data-table"> <thead> <tr> <th scope="col" class="col-checkbox"></th> <th scope="col">User</th> <th scope="col">Email</th> <th scope="col">Role</th> <th scope="col">Status</th> <th scope="col">Joined</th> </tr> </thead> <tbody> <tr> <td><div class="skeleton skeleton-checkbox"></div></td> <td> <div class="user-cell"> <div class="skeleton skeleton-avatar"></div> <div class="skeleton skeleton-line name"></div> </div> </td> <td><div class="skeleton skeleton-line email"></div></td> <td><div class="skeleton skeleton-line role"></div></td> <td><div class="skeleton skeleton-line status"></div></td> <td><div class="skeleton skeleton-line date"></div></td> </tr> <!-- Additional rows... --> </tbody> </table> </layout-card>

CSS Animation

The skeleton animation uses a simple pulse effect that fades between two opacity values. This is performant and works across all browsers.

@keyframes skeleton-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; } } .skeleton { background: var(--color-border); border-radius: var(--radius-s); animation: skeleton-pulse 1.5s ease-in-out infinite; } .skeleton-heading { height: 1.75rem; width: 60%; } .skeleton-line { height: 1rem; } .skeleton-line:nth-child(1) { width: 100%; } .skeleton-line:nth-child(2) { width: 100%; } .skeleton-line:nth-child(3) { width: 75%; } .skeleton-image { aspect-ratio: 16 / 9; width: 100%; border-radius: var(--radius-m); } .skeleton-avatar { width: 32px; height: 32px; border-radius: var(--radius-full); } .skeleton-button { height: 2.5rem; width: 8rem; border-radius: var(--radius-m); }

Configuration

Skeleton elements are built from CSS classes applied to placeholder elements. Here are the key styling options:

Class Purpose Customization
.skeleton Base class with animation Change animation-duration for speed
.skeleton-heading Heading placeholder Adjust height and width
.skeleton-line Text line placeholder Set width per line for variety
.skeleton-image Image placeholder Adjust aspect-ratio to match images
.skeleton-avatar Circular avatar placeholder Set width/height for size
.skeleton-button Button placeholder Match your button dimensions

Accessibility

  • Add aria-busy="true" to loading containers. Remove the attribute when content finishes loading.
  • Include an aria-label describing the loading state so screen readers announce meaningful context (e.g. "Loading card").
  • Use role="status" with a live region announcement when content finishes loading so assistive technology is notified.
  • Respect prefers-reduced-motion by disabling or reducing animations for users who prefer it.
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; opacity: 0.6; } }

Animation Variants

You can customize the animation effect to match your brand or preferences:

/* Default pulse animation */ @keyframes skeleton-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; } } /* Shimmer/wave animation (left-to-right) */ @keyframes skeleton-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .skeleton-shimmer { background: linear-gradient( 90deg, var(--color-border) 0%, var(--color-surface-raised) 50%, var(--color-border) 100% ); background-size: 200% 100%; animation: skeleton-shimmer 1.5s ease-in-out infinite; }

Usage Notes

  • Animation performance: The pulse animation only animates opacity, which is highly performant. Avoid animating layout properties.
  • Layout stability: Match skeleton dimensions to actual content to prevent layout shift (CLS) when content loads.
  • Duration: Use skeletons for loads under 3-5 seconds. For longer waits, consider progress indicators with estimated time.

Related

Feedback States

Unified empty, loading, and error states

Empty States

When there's no content to display

Error Pages

404, 500, and maintenance pages