List Skeleton
Four list rows [40 px avatar + 2 lines] in pure CSS: synchronized 1.8 s shimmer across 12 bones, line widths varying from row to row.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The code creates four .list-skel-item blocks inside a .list-skel container 280 px wide, arranged in a column (flex-direction: column, gap: 4px). Each item is a horizontal flex row with padding: 12px 16px, a rgba(255,255,255,.02) background and 12 px rounded corners. It holds a .skel-bone-circle .list-skel-avatar circle (40×40 px, flex-shrink: 0) and a text block .list-skel-text (flex: 1) with two bones: .list-skel-line1 (height 12 px, bottom margin 8 px) and .list-skel-line2 (height 9 px). The classes .skel-bone and .skel-bone-circle give each bone the rgba(255,255,255,.08) background and relative positioning needed by the shimmer pseudo-element. The HTML holds 12 animated bones in total (4 circles + 4 primary lines + 4 secondary lines).
Line widths vary from item to item via inline style attributes on the relevant bones: list-skel-line1 at 70% (CSS default), 60%, 80%, 50%; list-skel-line2 at 50% (CSS default), 40%, 55%, 35%. These variations are static and hard-coded in the HTML — no JavaScript generates them. The first item has no style attribute: its widths come from the CSS rules (.list-skel-line1 at 70%, .list-skel-line2 at 50%).
The shimmer is produced by each bone's ::after pseudo-element, at position: absolute; inset: 0, with background linear-gradient(90deg, transparent 0, rgba(255,255,255,.1) 50%, transparent 100%) on a background-size: 200% 100%. The shimmerSlide animation moves this background from background-position: -200% to 200% in 1.8 s ease-in-out, delay 0 s, infinite. All 12 bones share the same delay: the shimmer is synchronized, not staggered. Zero JavaScript — the entire effect is pure CSS.
Accessibility
- prefers-reduced-motion not handled: measured under
reducedMotion: 'reduce'(Playwright/Chromium), all 12 animations keep running (animationPlayState: running,animationDuration: 1.8s). Add@media (prefers-reduced-motion: reduce) { .skel-bone::after, .skel-bone-circle::after { animation: none } }. - The skeleton is a loading indicator, not content: set
role="status"oraria-busy="true"on the parent container of the real list (not on.list-skelitself), and remove.list-skelfrom the DOM once the content has arrived. No alt text is needed on the bones. - Set
aria-hidden="true"on.list-skel: the 12 empty bones must not be traversed by screen readers. Announce the end of loading through an existingaria-live="polite"region in your interface. - The bone background (
rgba(255,255,255,.08)) and shimmer flash (rgba(255,255,255,.1)) only make sense on a dark background. On a light background, bones and shimmer both become invisible: replace withrgba(0,0,0,.06)andrgba(0,0,0,.08).
Browser compatibility
Pure CSS, no JavaScript. Requires @keyframes, animation, the inset shorthand (Chrome 87+, Firefox 87+, Safari 14.1+), border-radius: 50%, display: flex and overflow: hidden. No dependencies.
Without CSS or on a browser older than Safari 14.1 (missing inset shorthand), the ::after pseudo-elements do not position themselves and the shimmer is invisible — the bones remain plain dark rectangles and circles with no animation, which is an acceptable fallback for a loading state.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="list-skel">
<div class="list-skel-item">
<div class="skel-bone-circle list-skel-avatar"></div>
<div class="list-skel-text">
<div class="skel-bone list-skel-line1"></div>
<div class="skel-bone list-skel-line2"></div>
</div>
</div>
<!-- … 3 more items, e.g. list-skel-line1 style="width:60%" and list-skel-line2 style="width:40%" -->
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Number of items (HTML — .list-skel-item blocks) |
4 | Duplicate or remove blocks. Each item adds ~65 px of height (40 px avatar + paddings + gap). For realistic line widths, vary the style="width:…" attributes from item to item. |
Avatar size (CSS — .list-skel-avatar { width; height }) |
40×40 px | The avatar is vertically centered in the item by align-items: center. At 48 px it represents a profile photo; at 32 px a compact icon. Change both properties together. |
Primary line height (CSS — .list-skel-line1 { height }) |
12 px | Targets the title or name in the row. 14px to align with body text at font-size: 14px; 10px for a denser list. |
Secondary line height (CSS — .list-skel-line2 { height }) |
9 px | Targets the subtitle, timestamp or summary. Keeping a ratio ≥ 0.7 relative to the primary line preserves the hierarchy when real content arrives. |
Line widths (HTML — style="width:…" on each bone) |
Line1: 70 / 60 / 80 / 50% — Line2: 50 / 40 / 55 / 35% | CSS default widths (70% / 50%) apply when the attribute is absent. Vary the values to simulate content of different lengths; values that are too similar make the list look uniform and less convincing. |
Gap between items (CSS — .list-skel { gap }) |
4 px | 0 = seamless list without visible separation (feed style); 8px = distinct entries, similar to an 8 px spacer between cards. Beyond 12 px, the list feel is lost. |
Shimmer duration (CSS — animation: 1.8s ease-in-out 0s infinite) |
1.8 s | Edit the duration in the animation declaration on the .skel-bone-circle::after, .skel-bone::after selector. 1.2s conveys urgency; 2.5s suggests a slow or lazy load. |
FAQ
animation: 1.8s ease-in-out 0s infinite, delay 0 s. For a cascading wave effect, add an increasing delay to each item: .list-skel-item:nth-child(2) .skel-bone::after, .list-skel-item:nth-child(2) .skel-bone-circle::after { animation-delay: .15s }, and so on with a 0.15 s increment per rank.rgba(255,255,255,.08) and the shimmer flash is rgba(255,255,255,.1) — two translucent whites, invisible on a white or light-grey surface. On a light background, replace these values with rgba(0,0,0,.06) and rgba(0,0,0,.08) in the .skel-bone, .skel-bone-circle and .skel-bone-circle::after, .skel-bone::after selectors.