SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/skeleton-loaders

8 CSS Skeleton Loaders

7 Pure CSS1 with JSPublished 2026-10-04Free · no sign-up

Eight skeleton screens that show the shape of content while it loads: a product card with a shimmer, a contact list, a full article, a table, a profile header, a product grid, a chat thread and a complete demo that swaps the skeleton for real content.

01. Skeleton Card with Shimmer

LoadingShimmer

A placeholder card that shows the shape of the content while it loads: an image block, a title line and two text lines. A light band sweeps across using an animated background gradient, and the card is marked aria-busy with a hidden Loading label.

View code
<div class="skc" aria-busy="true" aria-label="Loading product">
  <div class="sk img"></div>
  <div class="sk t"></div>
  <div class="sk l"></div>
  <div class="sk l s"></div>
</div>
.skc { display: grid; gap: 10px; width: 260px; padding: 14px; border-radius: 16px; background: #fff; box-shadow: 0 8px 24px -12px rgba(16,24,40,.25); }
.sk { border-radius: 8px; background: linear-gradient(90deg, #eef0f4 25%, #f8f9fb 50%, #eef0f4 75%) 0 0 / 300% 100%; animation: sk-sh 1.4s ease-in-out infinite; }
.sk.img { height: 150px; border-radius: 12px; }
.sk.t { height: 18px; width: 70%; margin-top: 4px; }
.sk.l { height: 12px; }
.sk.s { width: 50%; }
@keyframes sk-sh { from { background-position: 100% 0; } to { background-position: 0 0; } }
@media (prefers-reduced-motion: reduce) { .sk { animation: none; } }

02. Skeleton List with Avatars

ListLoading

Placeholder rows for a message or contact list, each with a round avatar and two lines of different widths. Varying the widths makes the skeleton look like real text instead of a grid of identical bars, which feels more natural.

View code
<ul class="skl" aria-busy="true" aria-label="Loading messages">
  <li><span class="a"></span><span class="b" style="width:60%"></span><span class="c" style="width:85%"></span></li>
  <li><span class="a"></span><span class="b" style="width:45%"></span><span class="c" style="width:70%"></span></li>
  <li><span class="a"></span><span class="b" style="width:55%"></span><span class="c" style="width:78%"></span></li>
</ul>
.skl { box-sizing: border-box; max-width: 100%; width: min(360px, 90vw); margin: 0; padding: 6px 16px; list-style: none; border-radius: 14px; background: #151a26; }
.skl li { display: grid; grid-template-columns: 42px 1fr; grid-template-rows: auto auto; gap: 8px 12px; padding: 12px 0; border-bottom: 1px solid #222838; }
.skl li:last-child { border-bottom: 0; }
.skl span { border-radius: 6px; background: #262d3e; animation: skl-p 1.6s ease-in-out infinite; }
.skl .a { grid-row: span 2; width: 42px; height: 42px; border-radius: 50%; }
.skl .b { height: 12px; }
.skl .c { height: 10px; }
@keyframes skl-p { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) { .skl span { animation: none; } }

03. Skeleton Article Page

BlogLoading

A full article placeholder: a wide hero image, a heading in two lines, author details and paragraphs. Using the real page layout for the skeleton means nothing jumps when the content arrives, which keeps the cumulative layout shift score low.

View code
<article class="ska" aria-busy="true" aria-label="Loading article">
  <div class="x hero"></div>
  <div class="x h1"></div><div class="x h1 short"></div>
  <div class="by"><div class="x av"></div><div class="x nm"></div></div>
  <div class="x p"></div><div class="x p"></div><div class="x p short"></div>
</article>
.ska { box-sizing: border-box; max-width: 100%; display: grid; gap: 10px; width: min(460px, 94vw); padding: 18px; border-radius: 16px; background: #fff; }
.x { border-radius: 6px; background: #eceef3; }
.hero { height: 130px; border-radius: 12px; margin-bottom: 6px; }
.h1 { height: 20px; }
.h1.short, .p.short { width: 60%; }
.by { display: flex; align-items: center; gap: 10px; margin: 6px 0; }
.av { width: 30px; height: 30px; border-radius: 50%; }
.nm { width: 120px; height: 10px; }
.p { height: 11px; }
.ska .x { position: relative; overflow: hidden; }
.ska .x::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent); animation: ska-w 1.5s infinite; }
@keyframes ska-w { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .ska .x::after { animation: none; display: none; } }

04. Skeleton Data Table

TableDashboard

A dashboard table placeholder with a header row and five body rows. Real column headers stay visible while the cells load, so people know what is coming, and the pulse animation is staggered by row with a custom property for a gentle wave.

View code
<table class="skt" aria-busy="true">
  <caption>Recent orders</caption>
  <thead><tr><th scope="col">Order</th><th scope="col">Customer</th><th scope="col">Total</th></tr></thead>
  <tbody>
    <tr style="--r:0"><td><i></i></td><td><i></i></td><td><i></i></td></tr>
    <tr style="--r:1"><td><i></i></td><td><i></i></td><td><i></i></td></tr>
    <tr style="--r:2"><td><i></i></td><td><i></i></td><td><i></i></td></tr>
    <tr style="--r:3"><td><i></i></td><td><i></i></td><td><i></i></td></tr>
  </tbody>
</table>
.skt { width: min(440px, 94vw); border-collapse: collapse; border-radius: 14px; overflow: hidden; background: #151a26; font: 600 13px Inter, system-ui, sans-serif; color: #aab3c7; }
.skt caption { padding: 0 0 10px; color: #fff; font-size: 15px; text-align: left; }
.skt th { padding: 12px 14px; background: #1b2131; text-align: left; }
.skt td { padding: 14px; border-top: 1px solid #222838; }
.skt i { display: block; height: 10px; border-radius: 5px; background: #2a3142; animation: skt-p 1.6s ease-in-out infinite; animation-delay: calc(var(--r) * 120ms); }
.skt td:nth-child(2) i { width: 80%; }
.skt td:nth-child(3) i { width: 50%; }
@keyframes skt-p { 50% { opacity: .4; } }
@media (prefers-reduced-motion: reduce) { .skt i { animation: none; } }

05. Skeleton Profile Header

ProfileLoading

A profile page placeholder with a cover banner, an overlapping round avatar, a name line and three stat blocks. The avatar overlap is done with a negative margin, matching the loaded design exactly so the swap from skeleton to content is seamless.

View code
<section class="skp" aria-busy="true" aria-label="Loading profile">
  <div class="cv"></div>
  <div class="av"></div>
  <div class="nm"></div>
  <div class="stats"><div></div><div></div><div></div></div>
</section>
.skp { width: min(380px, 90vw); overflow: hidden; border-radius: 16px; background: #fff; box-shadow: 0 8px 24px -12px rgba(16,24,40,.25); text-align: center; }
.skp div { background: #eceef3; }
.cv { height: 90px; background: linear-gradient(90deg, #e5e8ee, #f1f3f6) !important; }
.skp .av { width: 76px; height: 76px; margin: -38px auto 10px; border: 4px solid #fff; border-radius: 50%; }
.nm { width: 140px; height: 14px; margin: 0 auto 18px; border-radius: 7px; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 0 18px 18px; background: none !important; }
.stats div { height: 40px; border-radius: 10px; }
.skp div { animation: skp-p 1.6s ease-in-out infinite; }
@keyframes skp-p { 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .skp div { animation: none; } }

06. Skeleton Product Grid

E-commerceGrid

Four product placeholders in a responsive grid, the same columns as the real catalogue. Each tile has an image block, a name and a price bar; the shimmer is one gradient fixed to the viewport with background-attachment, so all tiles shimmer in sync.

View code
<div class="skg" aria-busy="true" aria-label="Loading products">
  <div class="tile"><div class="im"></div><div class="n"></div><div class="pr"></div></div>
  <div class="tile"><div class="im"></div><div class="n"></div><div class="pr"></div></div>
  <div class="tile"><div class="im"></div><div class="n"></div><div class="pr"></div></div>
  <div class="tile"><div class="im"></div><div class="n"></div><div class="pr"></div></div>
</div>
.skg { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; width: min(560px, 94vw); }
.tile { display: grid; gap: 8px; }
.skg .tile div { border-radius: 8px; background: linear-gradient(100deg, #1e2433 40%, #2c3448 50%, #1e2433 60%) fixed; background-size: 200% 100%; animation: skg-s 1.5s linear infinite; }
.im { aspect-ratio: 1; border-radius: 12px !important; }
.n { height: 12px; width: 80%; }
.pr { height: 12px; width: 40%; }
@keyframes skg-s { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { .skg .tile div { animation: none; } }

07. Skeleton to Content Swap

JavaScriptState

A card that shows its skeleton for a moment and then swaps in the real content, the full pattern rather than just the placeholder. aria-busy flips from true to false when the content arrives, and a Reload button lets you replay the transition.

View code
<div class="swap">
  <div class="card" id="sc" aria-busy="true" aria-live="polite">
    <div class="ph"><span></span><span></span><span class="s"></span></div>
    <div class="real" hidden><h3>Linen tote bag</h3><p>Natural fibre, 40 x 38 cm, inside pocket.</p><b>$32.00</b></div>
  </div>
  <button id="rl">Reload</button>
</div>
.swap { display: grid; gap: 12px; justify-items: start; font-family: Inter, system-ui, sans-serif; }
.card { width: 280px; min-height: 120px; padding: 18px; border-radius: 14px; background: #fff; box-shadow: 0 8px 24px -12px rgba(16,24,40,.25); }
.ph { display: grid; gap: 10px; }
.ph span { height: 14px; border-radius: 7px; background: #eceef3; animation: sw-p 1.4s ease-in-out infinite; }
.ph .s { width: 40%; }
.real h3 { margin: 0 0 6px; color: #101828; font-size: 17px; }
.real p { margin: 0 0 10px; color: #475467; font-size: 14px; }
.real b { color: #101828; }
.swap button { padding: 8px 14px; border: 1.5px solid #d0d5dd; border-radius: 10px; background: #fff; font: 700 13px Inter, sans-serif; cursor: pointer; }
@keyframes sw-p { 50% { opacity: .5; } }
@media (prefers-reduced-motion: reduce) { .ph span { animation: none; } }
const sc = document.getElementById('sc');
function load() {
  sc.setAttribute('aria-busy', 'true'); sc.querySelector('.ph').hidden = false; sc.querySelector('.real').hidden = true;
  setTimeout(() => { sc.querySelector('.ph').hidden = true; sc.querySelector('.real').hidden = false; sc.setAttribute('aria-busy', 'false'); }, 1600);
}
document.getElementById('rl').addEventListener('click', load);
load();

08. Skeleton Chat Thread

ChatLoading

Placeholder chat bubbles that alternate left and right, with different widths, to suggest a conversation loading. The bubble shapes match real messages, including the flattened corner on the speaker's side, so the loaded thread lines up exactly.

View code
<div class="skch" aria-busy="true" aria-label="Loading conversation">
  <span class="in" style="width:62%"></span>
  <span class="in" style="width:40%"></span>
  <span class="out" style="width:55%"></span>
  <span class="in" style="width:70%"></span>
  <span class="out" style="width:35%"></span>
</div>
.skch { box-sizing: border-box; max-width: 100%; display: flex; flex-direction: column; gap: 10px; width: min(340px, 90vw); padding: 16px; border-radius: 16px; background: #12161f; }
.skch span { height: 34px; border-radius: 16px; background: #262d3e; animation: skc-p 1.6s ease-in-out infinite; }
.skch .in { align-self: flex-start; border-bottom-left-radius: 4px; }
.skch .out { align-self: flex-end; border-bottom-right-radius: 4px; background: #2b3a6b; }
@keyframes skc-p { 50% { opacity: .5; } }
@media (prefers-reduced-motion: reduce) { .skch span { animation: none; } }

About these CSS Skeleton Loaders

Match the real layout

A skeleton should use the same sizes and spacing as the finished content, so nothing moves when the real data arrives. That keeps your Cumulative Layout Shift score low and makes loading feel faster.

Shimmer or pulse

A moving shimmer is an animated gradient over grey blocks; a pulse simply fades the blocks in and out. Both are fine; keep them subtle, and switch them off with prefers-reduced-motion.

Tell assistive technology

Skeleton blocks mean nothing to a screen reader. Mark the loading region with aria-busy="true" and a label such as Loading products, then set aria-busy to false when the content is in place.

Do not overuse them

Skeletons help when loading takes more than about half a second. For instant content they only add flicker, so show them after a short delay or only on slow requests.

Frequently asked questions

Are skeleton screens better than spinners?

For content-shaped areas such as cards and lists, usually yes, because they show what is coming and reduce perceived waiting.

Do these need JavaScript?

Only the swap demo, to simulate content arriving.

Can one shimmer cover several tiles?

Yes. The product grid uses a fixed background so every tile shimmers in sync.