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"oraria-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>