Skip to content

Skeleton

A placeholder UI that mimics the layout of content while it loads.

  • Skeleton Screen
  • Placeholder
  • Shimmer

Overview

The Skeleton (also called a skeleton screen, placeholder, shimmer, or content loader) is a feedback component that displays a simplified, placeholder version of the UI while actual content is loading. Unlike a Spinner that says "something is happening," a skeleton says "here's roughly what you're about to see."

Skeleton screens are the gold standard for perceived performance. Research by Luke Wroblewski, Bill Scott, and others demonstrates that users perceive skeleton-loaded interfaces as significantly faster than spinner-loaded ones — even when the actual load time is identical. This works because skeletons provide spatial predictability: the user can see where content will appear, begin scanning the layout, and mentally prepare for the incoming data.

The concept was popularized by Facebook (now Meta) in 2014 for their mobile news feed, and has since been adopted by virtually every major platform: YouTube, LinkedIn, Slack, Figma, Notion, and dozens more.

When to use a Skeleton:

  • Initial page loads where you know the content layout (lists, cards, profiles)
  • Data fetching within known container shapes (tables, grids, feed items)
  • Image placeholders while media loads
  • Transitioning between content states (navigating between pages in an SPA)
  • Any loading state longer than 1 second where you know the approximate layout

When NOT to use a Skeleton:

  • For unknown layouts where you can't predict the content shape — use a Spinner
  • For very short loads (< 300ms) — the skeleton will flash and feel janky
  • For operations with measurable progress — use a Progress Bar
  • For empty states — use an Empty State component
  • When the skeleton layout differs drastically from actual content — the "bait and switch" effect is worse than a spinner

Preview skeleton shimmer animations in the Animation & Easing Tool. Generate skeleton pulse/shimmer gradients with the Color Palette Generator. Explore skeleton loading patterns in the Loader Generator.

Variants

Shape Variants

VariantShapeUse Case
TextRounded rectangle, 14–16px tall, varying widthsBody text, labels, metadata
HeadingRounded rectangle, 20–32px tall, 40–70% widthPage titles, card headings
CirclePerfect circle, 32–64px diameterAvatars, profile pictures, icons
RectangleRounded rectangle with aspect ratioImages, thumbnails, video previews
CardFull card shape with internal skeleton elementsProduct cards, article cards
Table RowHorizontal set of rectangular skeletonsData table loading
Paragraph3–4 text skeletons stacked, last line shorterContent blocks, descriptions

Animation Variants

VariantAnimationPerformanceUse Case
Shimmer / WaveA light gradient sweeps left-to-right across the skeleton.Moderate (uses background-size animation or translateX).Most common. Facebook, YouTube, LinkedIn.
PulseThe skeleton fades between two opacity values.Lightweight (only opacity animates).Simple, low-overhead. Material Design default.
None / StaticNo animation — solid gray shape.Minimal.When prefers-reduced-motion is active.

The shimmer variant is more visually engaging but slightly more expensive to render. For pages with dozens of skeleton elements, the pulse variant may perform better. Preview both in the Animation & Easing Tool.

Composition Patterns

Rather than individual skeleton shapes, most implementations compose skeletons into templates that mirror real component layouts:

  • Card Skeleton: Rectangle (image) + heading skeleton + 2 text skeletons + circle (avatar)
  • List Item Skeleton: Circle (avatar) + heading + text
  • Table Skeleton: Grid of rectangles matching column widths
  • Article Skeleton: Large rectangle (hero) + heading + paragraph block
  • Profile Skeleton: Large circle (avatar) + heading + text + text

These composed templates are matched 1:1 to the real component, so the transition from skeleton to content is seamless.

Properties

Skeleton Properties

PropertyTypeDefaultDescription
variant'text' | 'heading' | 'circle' | 'rectangle' | 'custom''text'Shape of the skeleton element
widthstring | number'100%'Width of the skeleton. For text, use varying percentages (100%, 80%, 60%) for realism.
heightstring | number'1em'Height. For text, matches line height. For rectangles, set explicitly.
borderRadiusstring | number'4px'Corner rounding. Circles use '50%'. Preview with Border Radius Generator.
animation'shimmer' | 'pulse' | 'none''shimmer'Animation type. Honors prefers-reduced-motion automatically.
durationnumber1500Animation cycle duration in milliseconds
countnumber1Renders multiple skeleton lines (for paragraph-like blocks)
gapstring | number'8px'Gap between skeleton lines when count > 1
baseColorstring'var(--color-neutral-200)'Background color of the skeleton shape
highlightColorstring'var(--color-neutral-100)'Color of the shimmer highlight
isLoadingbooleantrueWhen false, renders children instead of skeleton. Enables inline usage.
childrenReactNode—Content to show when isLoading is false

Compound Component API

Some implementations (react-loading-skeleton, Chakra) offer a wrapper API:

<Skeleton isLoading={isLoading} fallback={<SkeletonCard />}>
  <ActualCard data={data} />
</Skeleton>

This pattern cleanly separates loading and loaded states without conditional rendering in the parent.

Token Mappings

Design Token Mappings

Skeletons use a narrow set of tokens — primarily color and animation. See the Design Tokens Complete Guide for token architecture.

Token CategoryToken ExampleSkeleton Usage
Color – Base--color-neutral-200Skeleton background fill
Color – Highlight--color-neutral-100 / --color-neutral-50Shimmer highlight color
Border Radius – Text--radius-sm (4px)Corner rounding for text shapes
Border Radius – Circle50%Avatar/icon skeletons
Border Radius – Card--radius-lg (12px)Card skeleton containers
Spacing – Line Gap--space-2 (8px)Gap between text skeleton lines
Spacing – Paragraph Gap--space-3 (12px)Gap between skeleton sections
Animation Duration--duration-skeleton (1.5s)One cycle of shimmer or pulse. Preview in Animation & Easing Tool.
Animation Timingease-in-outShimmer easing function

Dark Mode Considerations

Skeleton colors must invert for dark mode. The base should be slightly lighter than the page background, and the highlight slightly lighter still — just enough to be visible without being harsh:

/* Light mode */
--skeleton-base: var(--color-neutral-200);   /* #e5e7eb */
--skeleton-highlight: var(--color-neutral-100); /* #f3f4f6 */

/* Dark mode */
--skeleton-base: var(--color-neutral-800);   /* #1f2937 */
--skeleton-highlight: var(--color-neutral-700); /* #374151 */

Generate both light and dark skeleton palettes with the Color Palette Generator.

States

Skeleton States

Like Spinners, skeletons don't have interactive states (hover, focus, active). Their lifecycle is about visibility and transition:

StateBehaviorImplementation
Loading (Active)Skeleton is visible with animation running.Render skeleton elements matching the expected content layout.
TransitioningContent has loaded; skeleton morphs or fades into real content.Crossfade: fade skeleton out while fading content in. Duration: 200–300ms.
Loaded (Hidden)Content fully visible, skeleton removed from DOM.Replace skeleton with actual content. Don't leave invisible skeletons in DOM.
ErrorData failed to load.Replace skeleton with error UI or Empty State. Don't leave skeletons spinning forever.
Reduced MotionAnimation is paused or replaced with static gray shapes.Use prefers-reduced-motion to disable shimmer/pulse. Static shapes are still useful as placeholders.

Transition Best Practices

The transition from skeleton to content is the most important moment. Poor transitions (content popping in abruptly, layout shifts) can negate the perceived-performance benefits:

  1. Match dimensions exactly. The skeleton must be the same size as the real content. Any layout shift is jarring.
  2. Crossfade, don't swap. Fade the skeleton out (opacity 1→0) while fading content in (opacity 0→1). Duration: 200–300ms.
  3. Stagger content appearance. Load and reveal above-the-fold content first, then progressively reveal lower sections. This prevents a "flash of everything."
  4. Never show skeleton after content. If data loads faster than expected, skip the skeleton entirely (use a delay similar to the Spinner approach).

Accessibility

Accessibility — Skeleton

Skeletons are purely decorative placeholders — they have no interactive role and should be invisible to screen readers. The accessibility work is in communicating the loading state properly. Test skeleton-to-content contrast with the Contrast Checker.

ARIA Pattern

<!-- Region with skeleton loading -->
<div aria-busy="true" aria-label="Loading feed">
  <!-- Skeleton elements — hidden from AT -->
  <div class="skeleton skeleton-card" aria-hidden="true"></div>
  <div class="skeleton skeleton-card" aria-hidden="true"></div>
  <div class="skeleton skeleton-card" aria-hidden="true"></div>

  <!-- Screen reader announcement -->
  <div role="status" class="sr-only">Loading content…</div>
</div>

<!-- After loading completes -->
<div aria-busy="false">
  <article><!-- real content --></article>
  <article><!-- real content --></article>
</div>

Key points:

  • Mark all skeleton elements as aria-hidden="true" — they are decorative
  • Set aria-busy="true" on the container that will receive content
  • Provide a role="status" announcement so screen readers know loading is happening
  • When loading completes, set aria-busy="false" and remove the status message

WCAG Success Criteria

CriterionLevelRequirement for Skeletons
1.1.1 Non-text Content (A)ASkeleton shapes are decorative; use aria-hidden="true". The loading state must have a text alternative (role="status" announcement).
1.3.1 Info and Relationships (A)AThe relationship between loading state and content region must be conveyed via aria-busy on the container.
1.4.11 Non-text Contrast (AA)AASkeleton shapes should have at least 3:1 contrast against the page background so sighted users can perceive the placeholder. Use the Contrast Checker.
2.2.1 Timing Adjustable (A)AIf skeletons are tied to a timeout, provide an extension mechanism.
2.3.1 Three Flashes (A)AShimmer animations must not flash more than 3 times per second. A smooth left-to-right shimmer at 1.5s duration is well within limits.
2.3.3 Animation from Interactions (AAA)AAARespect prefers-reduced-motion — replace shimmer with static or gently pulsing placeholders.
4.1.3 Status Messages (AA)AAThe loading status must be communicated via role="status" so AT can announce it without focus.

Layout Stability (CLS)

Skeletons directly impact Core Web Vitals' Cumulative Layout Shift (CLS). The skeleton must reserve the exact space that content will occupy. If your skeleton is 200px tall but the real content is 350px, you'll get a layout shift when content loads — defeating the purpose. Measure your CLS with Chrome DevTools or Lighthouse and ensure skeleton-to-content transitions produce zero shift.

Usage Guidelines

Do

  • Match the skeleton to the real content layout precisely. Width, height, spacing, element count — all must mirror the actual UI. This is the fundamental rule.
  • Use varying widths for text skeletons. Don't make every line the same width. Use 100%, 85%, 60% to mimic natural text line lengths.
  • Keep skeletons simple. Gray shapes. No icons, no brand colors, no detail. The point is a silhouette, not a preview.
  • Apply shimmer/pulse animation for visual feedback that loading is active. Static skeletons can be mistaken for broken UI.
  • Combine with lazy loading. Show skeletons for below-the-fold content and load it as the user scrolls.
  • Set a minimum display time (300ms) to prevent the skeleton from flashing for fast loads.
  • Crossfade from skeleton to content. Don't let content "pop" in abruptly.

Don't

  • Don't show skeletons forever. If loading fails, transition to an error state. Set a maximum display time (15–30 seconds) before showing an error or retry option.
  • Don't use skeletons for unknown layouts. If you can't predict the content shape, use a Spinner.
  • Don't over-detail skeletons. Showing exact icon shapes, text lengths, or brand elements makes the transition feel like content changing rather than content loading.
  • Don't animate skeletons aggressively. Rapid or flashy shimmer is distracting and can fail WCAG 2.3.1.
  • Don't show skeleton + spinner together. Pick one approach. Skeleton screens replace spinners; they don't complement them.
  • Don't forget dark mode. Skeleton colors that work on white backgrounds disappear on dark backgrounds. Use semantic tokens.
  • Don't cause layout shifts. The skeleton must be pixel-perfect in dimensions relative to the content it represents. Measure with CLS tools.

Content Loading Strategies

StrategyApproachBest For
Component-level skeletonsEach component renders its own skeleton internallyReusable components (cards, list items)
Page-level skeletonsA full-page skeleton template for each routeSPA route transitions, SSR hydration
Progressive skeletonsLoad and reveal sections top-to-bottomLong pages, infinite feeds
Inline skeletonsSkeleton as children with isLoading toggleGranular control, per-field loading

Explore loading patterns and prebuilt skeleton animations in the Loader Generator.

Code Snippets

html
<!-- Single text skeleton -->
<div class="skeleton skeleton--text" aria-hidden="true"></div>

<!-- Paragraph skeleton (3 lines) -->
<div class="skeleton-paragraph" aria-hidden="true">
  <div class="skeleton skeleton--text" style="width: 100%"></div>
  <div class="skeleton skeleton--text" style="width: 85%"></div>
  <div class="skeleton skeleton--text" style="width: 60%"></div>
</div>

<!-- Card skeleton -->
<div class="skeleton-card" aria-hidden="true">
  <div class="skeleton skeleton--rect" style="height: 200px"></div>
  <div class="skeleton-card-body">
    <div class="skeleton skeleton--text" style="width: 70%; height: 20px"></div>
    <div class="skeleton skeleton--text" style="width: 100%"></div>
    <div class="skeleton skeleton--text" style="width: 90%"></div>
    <div style="display: flex; align-items: center; gap: 8px; margin-top: 12px">
      <div class="skeleton skeleton--circle" style="width: 32px; height: 32px"></div>
      <div class="skeleton skeleton--text" style="width: 120px"></div>
    </div>
  </div>
</div>

<!-- Loading region with screen reader support -->
<div aria-busy="true">
  <div class="skeleton-card" aria-hidden="true"><!-- ... --></div>
  <div role="status" class="sr-only">Loading content…</div>
</div>

<style>
  .skeleton {
    background: var(--color-neutral-200);
    border-radius: 4px;
    position: relative;
    overflow: hidden;
  }
  .skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
      90deg,
      transparent 0%,
      var(--color-neutral-100) 50%,
      transparent 100%
    );
    animation: shimmer 1.5s ease-in-out infinite;
    transform: translateX(-100%);
  }
  @keyframes shimmer {
    100% { transform: translateX(100%); }
  }
  .skeleton--text {
    height: 14px;
    margin-bottom: 8px;
  }
  .skeleton--rect {
    width: 100%;
    border-radius: 8px 8px 0 0;
  }
  .skeleton--circle {
    border-radius: 50%;
  }
  .skeleton-card {
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid var(--color-neutral-200);
  }
  .skeleton-card-body {
    padding: 16px;
  }
  .sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  @media (prefers-reduced-motion: reduce) {
    .skeleton::after { animation: none; }
  }
</style>
tsx
import { forwardRef } from "react";

interface SkeletonProps {
  variant?: "text" | "heading" | "circle" | "rectangle";
  width?: string | number;
  height?: string | number;
  borderRadius?: string | number;
  animation?: "shimmer" | "pulse" | "none";
  count?: number;
  gap?: string | number;
  baseColor?: string;
  highlightColor?: string;
  isLoading?: boolean;
  className?: string;
  children?: React.ReactNode;
}

export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
  (
    {
      variant = "text",
      width,
      height,
      borderRadius,
      animation = "shimmer",
      count = 1,
      gap = 8,
      baseColor = "var(--color-neutral-200)",
      highlightColor = "var(--color-neutral-100)",
      isLoading = true,
      className,
      children,
      ...props
    },
    ref
  ) => {
    if (!isLoading && children) return <>{children}</>;

    const variantStyles: Record<string, React.CSSProperties> = {
      text: { height: height ?? 14, width: width ?? "100%", borderRadius: borderRadius ?? 4 },
      heading: { height: height ?? 24, width: width ?? "60%", borderRadius: borderRadius ?? 4 },
      circle: {
        height: height ?? 40,
        width: width ?? 40,
        borderRadius: "50%",
      },
      rectangle: { height: height ?? 200, width: width ?? "100%", borderRadius: borderRadius ?? 8 },
    };

    const style = variantStyles[variant];

    const elements = Array.from({ length: count }, (_, i) => (
      <div
        key={i}
        aria-hidden="true"
        className={className}
        style={{
          ...style,
          background: baseColor,
          position: "relative" as const,
          overflow: "hidden" as const,
          ...(i < count - 1 && variant === "text" ? {} : {}),
        }}
        {...props}
      >
        {animation === "shimmer" && (
          <div
            style={{
              position: "absolute",
              inset: 0,
              background: `linear-gradient(90deg, transparent 0%, ${highlightColor} 50%, transparent 100%)`,
              animation: "shimmer 1.5s ease-in-out infinite",
              transform: "translateX(-100%)",
            }}
          />
        )}
      </div>
    ));

    return (
      <div ref={ref} style={{ display: "flex", flexDirection: "column", gap }}>
        {elements}
      </div>
    );
  }
);

Skeleton.displayName = "Skeleton";

Design Systems

Design System Implementations

Material Design 3 doesn't have a dedicated "skeleton" component in its spec — instead, it recommends using a container with a Placeholder layer that uses a subtle pulse animation (opacity oscillation between 0.04 and 0.12 on neutral tones). Material's approach is more restrained than the shimmer pattern — just a gentle breathing effect. This aligns with Material's philosophy of calm, non-distracting loading states.

Ant Design's Skeleton component is one of the most fully-featured implementations. It offers Skeleton.Avatar, Skeleton.Title, Skeleton.Paragraph, and Skeleton.Button sub-components that compose into realistic loading templates. The active prop enables shimmer animation, and the loading prop wraps real content — when loading becomes false, the skeleton seamlessly transitions to children. It also supports round shapes and custom paragraph row counts/widths.

react-loading-skeleton (an independent library, not a design system) is the de facto standard for React skeleton loading. It uses CSS @keyframes with background-position for the shimmer, provides <Skeleton count={5} /> for multi-line blocks, supports circle for avatars, and automatically adapts to parent width. Its SkeletonTheme provider allows global color customization.

Chakra UI provides a Skeleton component with startColor and endColor props for the animation gradient. It also offers SkeletonText (renders multiple lines with the last line shorter) and SkeletonCircle. The isLoaded prop triggers a smooth crossfade from skeleton to content — one of the cleanest transition implementations.

Shadcn/ui offers a bare-minimum Skeleton — just a <div> with animate-pulse (Tailwind's built-in pulse animation) and a muted background. No shimmer, no sub-components, no isLoading wrapper. You compose templates manually.

Bootstrap doesn't include skeletons in its core library. The community typically implements them using Bootstrap's existing utility classes or third-party plugins.

For creating shimmer and pulse animations with precise timing, use the Animation & Easing Tool. Generate skeleton color pairs (base + highlight) for both light and dark themes with the Color Palette Generator. Browse prebuilt skeleton patterns in the Loader Generator.

FeedbackSkeleton ScreenPlaceholderShimmer