Skeleton Profile — Free HTML CSS JS Loading Placeholder Card
Skeleton Profile · Loaders · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="sk-card" id="skCard">
<div class="sk-cover"></div>
<div class="sk-body">
<div class="sk-avatar"></div>
<div class="sk-line sk-w60"></div>
<div class="sk-line sk-w40"></div>
<div class="sk-stats">
<div class="sk-stat"></div>
<div class="sk-stat"></div>
<div class="sk-stat"></div>
</div>
<div class="sk-line sk-w90"></div>
<div class="sk-line sk-w80"></div>
<div class="sk-actions"><div class="sk-btn"></div><div class="sk-btn sk-ghost"></div></div>
</div>
</div>
<button type="button" class="sk-toggle" id="skToggle">Load content</button>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0c0e16;display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:100vh;gap:18px;padding:24px}
.sk-card{width:300px;background:#141826;border:1px solid #222838;border-radius:18px;overflow:hidden}
.sk-cover{height:80px;background:#1b2030}
.sk-body{padding:0 18px 18px;margin-top:-30px}
.sk-avatar{width:64px;height:64px;border-radius:50%;background:#1b2030;border:4px solid #141826}
.sk-line{height:12px;border-radius:6px;background:#1b2030;margin-top:12px}
.sk-w90{width:90%}.sk-w80{width:80%}.sk-w60{width:60%}.sk-w40{width:40%}
.sk-stats{display:flex;gap:10px;margin-top:16px}
.sk-stat{flex:1;height:44px;border-radius:10px;background:#1b2030}
.sk-actions{display:flex;gap:10px;margin-top:18px}
.sk-btn{flex:1;height:38px;border-radius:10px;background:#1b2030}
/* Shimmer sweep shared by every placeholder block. */
.sk-cover,.sk-avatar,.sk-line,.sk-stat,.sk-btn{position:relative;overflow:hidden}
.sk-cover::after,.sk-avatar::after,.sk-line::after,.sk-stat::after,.sk-btn::after{content:'';position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,rgba(255,255,255,.07),transparent);animation:skShimmer 1.4s infinite}
@keyframes skShimmer{100%{transform:translateX(100%)}}
/* Loaded state: real content fades in, skeleton fades out. */
.sk-card.is-loaded *::after{animation:none;content:none}
.sk-card.is-loaded .sk-cover{background:linear-gradient(120deg,#6366f1,#8b5cf6)}
.sk-card.is-loaded .sk-avatar{background:#0ea5e9}
.sk-card.is-loaded .sk-line,.sk-card.is-loaded .sk-stat,.sk-card.is-loaded .sk-btn{background:#222a3c}
.sk-card.is-loaded .sk-btn{background:#6366f1}
.sk-card.is-loaded .sk-ghost{background:#222a3c}
.sk-toggle{background:#1d2233;border:1px solid #2c3346;color:#cdd3e4;font-family:inherit;font-size:13px;font-weight:600;padding:9px 16px;border-radius:10px;cursor:pointer}
@media(prefers-reduced-motion:reduce){.sk-cover::after,.sk-avatar::after,.sk-line::after,.sk-stat::after,.sk-btn::after{animation:none}}var card = document.getElementById('skCard');
var toggle = document.getElementById('skToggle');
toggle.addEventListener('click', function () {
var loaded = card.classList.toggle('is-loaded');
toggle.textContent = loaded ? 'Show skeleton' : 'Load content';
});
// In a real app you would add 'is-loaded' once your fetch resolves:
// fetch('/api/profile').then(function () { card.classList.add('is-loaded'); });Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Skeleton Profile — A Shimmering Profile-Card Placeholder

The skeleton profile is the grey, shimmering placeholder a profile card shows while its data loads — the cover, avatar, name, stats, and buttons all rendered as soft pulsing blocks so the layout is visible before the content arrives. This snippet builds it with plain HTML and CSS, plus a tiny toggle to demo the swap to real content.
Structure mirrors the real card
The skeleton's blocks are laid out exactly where the finished card's elements will be: an 80px cover band, a 64px round avatar overlapping it with a negative margin, name and subtitle lines of different widths, a three-up stats row, body lines, and two action buttons. Because the placeholder matches the real layout, the content doesn't jump or reflow when it loads — the whole point of a skeleton over a spinner is that the page geometry is already in place.
The shimmer sweep
Every placeholder block shares one shimmer: a ::after pseudo-element holds a horizontal linear-gradient highlight that animates with transform: translateX from -100% to 100% on a loop. Using a transform (rather than animating background-position) keeps the sweep on the GPU and perfectly smooth. One @keyframes and one shared rule drive the cover, avatar, lines, stats, and buttons together, so the entire card shimmers in sync.
Varying widths sell the realism
The text lines use utility width classes (sk-w90, sk-w60, sk-w40, etc.) so they look like real lines of differing length rather than identical bars. That small variety is what makes a skeleton read as "content loading" instead of "broken layout" — uniform bars look like an error, staggered widths look like text.
The loaded swap
Toggling an is-loaded class stops the shimmer (animation: none) and recolours the blocks into a finished-looking card — a gradient cover, a coloured avatar, and a primary button. In a real app you'd add that class the moment your fetch resolves; here a button toggles it so you can see both states. This models the exact handoff from placeholder to content.
Respecting reduced motion
A prefers-reduced-motion: reduce media query disables the shimmer for users who are sensitive to movement, leaving a static skeleton. Honouring that preference is an accessibility baseline for any looping animation, and it's a one-rule addition here.
Customizing it
Match the block sizes to your real card, change the shimmer colour and speed, or add more lines. Drop the loaded-state styles and instead render your actual content over the same skeleton container. Pair it with a skeleton card grid, a skeleton loader, or a skeleton dashboard.
Step by step
How to Use
- 1Paste HTML and CSSA shimmering profile-card skeleton renders.
- 2Watch the sweepA highlight glides across every block in sync.
- 3Click Load contentThe shimmer stops and the card colours in.
- 4Toggle backReturn to the skeleton state to compare.
- 5Wire to a fetchAdd is-loaded when your data resolves.
- 6Match your cardResize the blocks to your real layout.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A skeleton lays its placeholder blocks exactly where the real elements will be — cover, avatar, name, stats, buttons — so the page geometry is already in place and content does not jump or reflow when it loads. A spinner gives no sense of the layout and the page lurches when data arrives.
Each block has an ::after holding a horizontal gradient highlight that animates with transform: translateX from -100% to 100% on a loop. Using a transform keeps the sweep on the GPU compositor and perfectly smooth, and one shared keyframes and rule drive every block so the whole card shimmers in sync.
The text lines use utility width classes so they look like real lines of differing length. Uniform bars read as a broken layout, while staggered widths read as text that is loading — that small variety is what sells the skeleton as content arriving rather than an error state.
Yes. A prefers-reduced-motion: reduce media query disables the shimmer animation, leaving a static skeleton for users sensitive to movement. Honouring that preference is an accessibility baseline for any looping animation, and here it is a single rule.
Render the skeleton while a loading flag is true and the real card when it is false, or keep the same container and add an is-loaded class when your fetch resolves. Tie the flag to your data-fetching state (a loading boolean, React Query's isLoading, a Suspense fallback). The shimmer CSS ports unchanged; in Tailwind use animate utilities or a custom keyframe.