Profile Skeleton
Ten CSS bones in a column: an 80 px circle, a name (130×16 px), two bio lines and three stats blocks — synchronous 1.8 s shimmer, zero JavaScript.
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 HTML exposes a single column: .profile-skel (260 px wide, flex column, centered items, gap: 12 px) holds four bones in the main column, then .profile-skel-stats (flex row, gap: 20 px, margin-top: 8 px) lays out three blocks side by side. Two classes carry the bones: .skel-bone for rectangles (border-radius: 6 px) and .skel-bone-circle for the avatar (border-radius: 50%). Measured dimensions: avatar 80×80 px, name 130×16 px, bio1 200×10 px, bio2 160×10 px — the two different widths suggest natural text. Each stats block holds a number bone (36×18 px, border-radius: 4 px) and a label bone (48×9 px, border-radius: 4 px). Base fill: rgba(255,255,255,.08). Zero JavaScript.
The shimmer lives entirely in pseudo-elements: .skel-bone::after and .skel-bone-circle::after cover each bone (position: absolute; inset: 0) and carry a linear-gradient(90deg, transparent 0, rgba(255,255,255,.1) 50%, transparent 100%) set to 200% of the element width. The shimmerSlide animation moves background-position from -200% 0 to 200% 0: a centered bright stripe travels left to right across the bone, fades out, and repeats. Duration: 1.8 s, ease-in-out, 0 s delay, infinite loop.
All ten bones share a 0 s delay, so the shimmer crosses them all at once. Across the 260 px column, the bright stripe sweeps the entire profile simultaneously, with no top-to-bottom offset. The shimmer peak is rgba(255,255,255,.10) against a base fill of rgba(255,255,255,.08) — a two-point opacity gap, subtle on a very dark background (#0a0a0f). On a lighter surface or a light-theme page the contrast between bone and shimmer disappears: switch to rgba(0,0,0,.06) for the bone fill and rgba(0,0,0,.12) for the shimmer peak.
Accessibility
- prefers-reduced-motion not handled. Measured with Playwright's
reduceemulation:animationPlayStatestaysrunningon all ten bones. Add@media (prefers-reduced-motion: reduce) { .skel-bone::after, .skel-bone-circle::after { animation: none; } }— the bones stay visible (8% fill), only the sweep stops. - The skeleton is a loading indicator: the code carries no
role="status", noaria-busy, noaria-live, and no text alternative. Wrap.profile-skelin a<div role="status" aria-label="Loading profile…">, or put those attributes on the parent container that will swap the skeleton out for real content. - All bones are empty, non-focusable
divelements. Addaria-hidden="true"on.profile-skelwhen therole="status"is carried by a parent. - The width is hard-coded to 260 px. On a column narrower than 280 px — typical on mobile — the skeleton overflows. Replace
width: 260pxwithwidth: min(260px, 100%)and adjust the individual bone widths to percentages ormax-width.
Browser compatibility
Pure CSS: requires @keyframes, animation, linear-gradient, background-size, flexbox and ::after pseudo-elements. Zero JavaScript, zero dependencies.
Without CSS (HTML email, RSS reader), the bones vanish and the area is empty. Without @keyframes (IE 9 and earlier), bones render as solid grey shapes with no sweep — a static placeholder. Without flexbox (IE 9 and earlier), the column and the stats row lose their layout.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="profile-skel">
<div class="skel-bone-circle profile-skel-avatar"></div>
<div class="skel-bone profile-skel-name"></div>
<div class="skel-bone profile-skel-bio1"></div>
<div class="skel-bone profile-skel-bio2"></div>
<div class="profile-skel-stats">
<div class="profile-skel-stat">
<div class="skel-bone profile-skel-stat-num"></div>
<div class="skel-bone profile-skel-stat-label"></div>
</div>
<div class="profile-skel-stat">
<div class="skel-bone profile-skel-stat-num"></div>
<div class="skel-bone profile-skel-stat-label"></div>
</div>
<!-- … 1 autre .profile-skel-stat (3 au total) -->
</div>
</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 |
|---|---|---|
Avatar size (CSS — .profile-skel-avatar) |
80×80 px | Circle size. Width, height and border-radius: 50% are linked — keep them equal. 64 px suits a compact card; 96 px a full profile page. |
Name width (CSS — .profile-skel-name) |
130 px × 16 px | Suggests the length of the username or full name. 100 px for a short handle; 180 px for a long name. The 16 px height matches a typical body text line. |
Bio line widths (CSS — .profile-skel-bio1, .profile-skel-bio2) |
200 px / 160 px | The asymmetry (200/160) mimics a natural sentence break. Two equal lines look artificial. Remove .profile-skel-bio2 if the bio fits on a single line. |
Shimmer duration (CSS — animation: 1.8s) |
1.8 s | Speed of the sweep. 1.2 s feels urgent; 2.4 s communicates a longer wait. Below 1 s the sweep looks jittery. |
Bone base fill (CSS — rgba(255,255,255,.08)) |
rgba(255,255,255,.08) | Fill color for rectangles and circle. On a light background, switch to rgba(0,0,0,.07). On a colored surface, use the page's dominant hue at 10–15% opacity. |
Shimmer peak (CSS — rgba(255,255,255,.1) inside the gradient) |
rgba(255,255,255,.10) | Brightness of the bright stripe. Increasing the gap with the base fill (e.g. .18) makes the sweep more visible. On a light background, replace with rgba(255,255,255,.9). |
Number of stats blocks (.profile-skel-stat) |
3 | Remove one .profile-skel-stat for two metrics (posts, followers), or add a fourth if the page shows four counters. The flex parent redistributes them automatically. |
FAQ
animation-delay to each bone. Example: name 0 s, bio1 0.1 s, bio2 0.2 s, stats 0.3 s. The bright stripe then descends from top to bottom, as if scanning the profile. A short stagger (0.1 s) creates a scan impression; a longer one (0.3–0.5 s) makes a slow wave. The synchronous default is more understated and does not draw the eye to one bone at a time.width: 260px on .profile-skel with width: 100%; max-width: 260px, and switch fixed bone widths to percentages: 50% for the name, 77% for bio1, 62% for bio2. The stats row is already flex — just check that .profile-skel-stats stays flex-wrap: nowrap on mobile (three narrow columns) or switch to flex-wrap: wrap; gap: 12px for three independent blocks.