You Might Also Like
Skeleton Profile — Free HTML CSS JS Loading Placeholder Card
Skeleton Profile · Loaders · Plain HTML, CSS & JS · Live preview
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.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to work out the shimmer geometry or the is-loaded swap by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the shimmer uses a transform: translateX sweep on a shared ::after pseudo-element rather than animating background-position like other skeleton snippets, or why every placeholder block shares one keyframe instead of running independent timers. The same assistant can help optimize it, for instance checking whether the wildcard *::after selector used to kill the shimmer on is-loaded could be scoped more narrowly for large card grids. It is just as useful for extending it: ask it to drive the is-loaded class from a real fetch promise instead of a button, add a skeleton error state for failed loads, or generalize the block sizes into CSS variables so the same skeleton fits multiple card layouts. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a shimmering "skeleton profile card" placeholder in plain HTML, CSS, and a small amount of JavaScript — no libraries.
Requirements:
- A profile-card layout with a cover band, a circular avatar that visually overlaps the cover using a negative top margin, name and subtitle placeholder lines of different widths, a three-column stats row, two body text lines, and two action-button placeholders — every block positioned exactly where the real, loaded card's content will appear.
- Implement the shimmer as a single shared technique: give every placeholder block a ::after pseudo-element containing a horizontal linear-gradient highlight, and animate it purely with a CSS transform (translateX from -100% to 100%), not background-position — so the sweep runs on the compositor and every block animates from the same one keyframe rule in sync.
- The text-line placeholders must use varied width utility classes (not all identical width) so they read as text of differing length rather than a uniform stack of bars.
- A toggle (button click, standing in for a real fetch resolving) must add an is-loaded class to the card that: stops every shimmer animation at once, and recolors the cover, avatar, lines, stats, and buttons into a finished-looking card (gradient cover, colored avatar, solid primary button) — implemented with as few CSS rules as possible by leaning on the shared .is-loaded ancestor selector.
- Respect prefers-reduced-motion by disabling the shimmer animation entirely for users who request reduced motion, leaving a static (non-animated) placeholder instead.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
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.