CSS-Only

Skeleton

Loading placeholder component with configurable shapes and animation styles for better perceived performance. Pure CSS with no JavaScript required.

Example

<div aria-busy="true">
  <div class="omni-skeleton" data-shape="text" style="width: 80%;"></div>
  <div class="omni-skeleton" data-shape="text" style="width: 60%;"></div>
  <div class="omni-skeleton" data-shape="text" style="width: 70%;"></div>
</div>

Shapes

Text

Use data-shape="text" for text line placeholders. Inherits font size (1em height) with automatic vertical spacing:

<div class="omni-skeleton" data-shape="text" style="width: 100%;"></div>
<div class="omni-skeleton" data-shape="text" style="width: 95%;"></div>
<div class="omni-skeleton" data-shape="text" style="width: 80%;"></div>
<div class="omni-skeleton" data-shape="text" style="width: 60%;"></div>

Circle

Use data-shape="circle" for avatar placeholders. Automatically maintains 1:1 aspect ratio:

<div class="omni-skeleton" data-shape="circle" style="width: 2rem;"></div>
<div class="omni-skeleton" data-shape="circle" style="width: 3rem;"></div>
<div class="omni-skeleton" data-shape="circle" style="width: 4rem;"></div>

Rectangle

Use data-shape="rect" for video/image placeholders. Defaults to 16:9 aspect ratio:

<div class="omni-skeleton" data-shape="rect" style="width: 100%;"></div>

Rounded

Use data-shape="rounded" for card images and thumbnails with larger border radius:

<div class="omni-skeleton" data-shape="rounded" style="width: 100%; height: 200px;"></div>

Default (No Shape)

Without data-shape, skeleton uses small border radius and respects explicit dimensions:

<div class="omni-skeleton" style="width: 100px; height: 100px;"></div>
<div class="omni-skeleton" style="width: 150px; height: 80px;"></div>

Animation Variants

Pulse (default)

Default pulse animation provides subtle fade in/out effect:

<!-- Default pulse animation -->
<div class="omni-skeleton" data-shape="text" style="width: 80%;"></div>

Shimmer

Use data-variant="shimmer" for a gradient wave effect:

<div class="omni-skeleton" data-variant="shimmer" data-shape="text" style="width: 80%;"></div>
<div class="omni-skeleton" data-variant="shimmer" data-shape="text" style="width: 60%;"></div>
<div class="omni-skeleton" data-variant="shimmer" data-shape="text" style="width: 70%;"></div>

Common Patterns

User Profile Card

<div aria-busy="true">
  <div style="display: flex; gap: 1rem; align-items: center;">
    <div class="omni-skeleton" data-shape="circle" style="width: 4rem;"></div>
    <div style="flex: 1;">
      <div class="omni-skeleton" data-shape="text" style="width: 60%;"></div>
      <div class="omni-skeleton" data-shape="text" style="width: 40%;"></div>
    </div>
  </div>
  <div class="omni-skeleton" data-shape="text" style="width: 100%;"></div>
  <div class="omni-skeleton" data-shape="text" style="width: 95%;"></div>
  <div class="omni-skeleton" data-shape="text" style="width: 80%;"></div>
</div>

Article List

<div aria-busy="true">
  <div style="display: flex; gap: 1rem;">
    <div class="omni-skeleton" data-shape="rounded" style="width: 120px; height: 80px;"></div>
    <div style="flex: 1;">
      <div class="omni-skeleton" data-shape="text" style="width: 70%;"></div>
      <div class="omni-skeleton" data-shape="text" style="width: 100%;"></div>
      <div class="omni-skeleton" data-shape="text" style="width: 40%;"></div>
    </div>
  </div>
</div>

Image Gallery

<div aria-busy="true" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 1rem;">
  <div class="omni-skeleton" data-shape="rounded" style="width: 150px; height: 150px;"></div>
  <div class="omni-skeleton" data-shape="rounded" style="width: 150px; height: 150px;"></div>
  <div class="omni-skeleton" data-shape="rounded" style="width: 150px; height: 150px;"></div>
  <div class="omni-skeleton" data-shape="rounded" style="width: 150px; height: 150px;"></div>
</div>

API Reference

Attribute Values Default Description
data-shape text, circle, rect, rounded Skeleton shape form factor
data-variant shimmer pulse Animation style (pulse is default)

Shape Characteristics

Shape Aspect Ratio Border Radius Best For
text Auto (1em height) Small Paragraph lines, headings
circle 1:1 (auto) Full (50%) Avatars, profile images
rect 16:9 (auto) Small Videos, hero images
rounded Manual Large Card images, thumbnails
(none) Manual Small Custom shapes

Best Practices

When to Use

  • Content loading: Display during initial page load or dynamic content fetch
  • Perceived performance: Improve user experience during >300ms loads
  • Layout stability: Reserve space to prevent layout shifts when content loads
  • Progressive loading: Show skeleton for parts of the page while others load

When NOT to Use

  • Fast loads (<300ms): Skeleton flash can be more jarring than brief wait
  • Interactive elements: Don't use for buttons, inputs, modals, dropdowns
  • Critical actions: Use spinner for form submissions, data saves
  • Small updates: Inline spinners work better for partial refreshes

Implementation Tips

  • Match layout: Skeleton should mirror the actual content layout
  • Vary widths: Use different widths (60%, 80%, 100%) for text lines to look natural
  • Use inline styles: Width and height via inline styles for flexibility
  • Group skeletons: Wrap related skeletons in containers for better structure

Accessibility

  • Parent container: Add aria-busy="true" to the parent container during loading
  • Screen readers: Consider aria-label="Loading content" or aria-live="polite" on parent
  • Hidden from AT: Skeleton elements themselves don't need ARIA labels (purely visual)
  • Reduced motion: Respects prefers-reduced-motion - animations disabled, opacity reduced to 0.6
  • State updates: Remove aria-busy="true" when content loads
  • Content replacement: Replace skeleton HTML with actual content (don't hide/show)

Accessibility Example

<!-- While loading -->
<div aria-busy="true" aria-label="Loading user profile">
  <div class="omni-skeleton" data-shape="circle" style="width: 4rem;"></div>
  <div class="omni-skeleton" data-shape="text" style="width: 60%;"></div>
</div>

<!-- After content loads (replace HTML, don't just hide skeleton) -->
<div>
  <img src="avatar.jpg" alt="User name">
  <h2>User Name</h2>
</div>