16 / 22
All 22 in 22 CSS Skeleton Loaders
- 01React Suspense Fallback Skeleton
- 02Next.js Loading UI Skeleton
- 03Skeleton with prefers-reduced-motion Fallback
- 04Dark Mode Skeleton with color-mix Tokens
- 05SVG Placeholder Blur-Up Image Skeleton
- 06Content-Aware Skeleton Sizing with clamp
- 07Skeleton to Content Cross-Fade Transition
- 08ARIA Live Region Loading Announcement
- 09Skeleton Timing and Duration Recipe
- 10Layout-Preserving Skeleton with min-height
- 11Classic Shimmer Skeleton Loader
- 12Pulse Block Skeleton Loader
- 13Avatar Profile Skeleton Loader
- 14Image Placeholder Skeleton Loader
- 15Wave Sweep Skeleton Loader
- 16Comment Thread Skeleton Loader
- 17Article Page Skeleton Loader
- 18Data Table Row Skeleton Loader
- 19Stat Tiles Dashboard Skeleton Loader
- 20Gradient Glow Skeleton Loader
- 21Media List Skeleton with Thumbnails
- 22Chip Cluster Filter Skeleton Loader
Comment Thread Skeleton Loader
Threads load after the post, so the skeleton sits under real content and has to match its rhythm exactly. Three comment shells — avatar, name, meta, three text lines — plus one indented reply, all built from a real <ul> with the same indent and vertical spacing the resolved thread uses.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<div class="sk-16">
<div class="sk-16__stage">
<section class="sk-16__wrap" aria-labelledby="sk-16-h">
<header class="sk-16__head">
<p class="sk-16__eyebrow">thread · 3 shells + 1 reply</p>
<h2 class="sk-16__h" id="sk-16-h">The post is real. The thread is pending.</h2>
<p class="sk-16__sub">Comments load after the article, so the skeleton has to inherit the resolved thread's gaps and indent — otherwise the page re-rhythms when replies land.</p>
</header>
<article class="sk-16__post">
<div class="sk-16__prow">
<img class="sk-16__pic" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=96&h=96&q=70" width="96" height="96" alt="Alex Chen" loading="lazy" decoding="async">
<div class="sk-16__ident">
<p class="sk-16__name">Alex Chen</p>
<p class="sk-16__meta">2 hours ago · 24 replies</p>
</div>
</div>
<p class="sk-16__copy">We replaced every spinner in the review flow with a skeleton that matches its resolved layout. Reviewers stopped reporting “the page jumped” within a week.</p>
</article>
<ul class="sk-16__thread" role="status" aria-live="polite">
<span class="sk-16__vh">Loading 3 comments…</span>
<li class="sk-16__comment" style="--i:0">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:8rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:6.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:92%"></span><span class="sk-16__bar" style="inline-size:58%"></span></div>
</article>
</li>
<li class="sk-16__comment" style="--i:1">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:10rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:5.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:86%"></span><span class="sk-16__bar" style="inline-size:44%"></span></div>
</article>
<ul class="sk-16__replies">
<li class="sk-16__comment" style="--i:2">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar sk-16__avatar--sm"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:7rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:4.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:52%"></span></div>
</article>
</li>
</ul>
</li>
<li class="sk-16__comment" style="--i:3">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:9rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:7rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:90%"></span><span class="sk-16__bar" style="inline-size:62%"></span></div>
</article>
</li>
</ul>
<aside class="sk-16__spec" aria-label="Recipe">
<p class="sk-16__srow2"><span class="sk-16__skey">rhythm</span><span class="sk-16__sval">thread gap 1.35rem · line gap .55rem — identical to the resolved thread</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">indent</span><span class="sk-16__sval">padding-inline-start: clamp(1.5rem, 4vw, 2.75rem) + a 1px rail</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">stagger</span><span class="sk-16__sval">animation-delay: calc(var(--i) * -.22s) per comment, never per bar</span></p>
</aside>
</section>
</div>
</div><div class="sk-16">
<div class="sk-16__stage">
<section class="sk-16__wrap" aria-labelledby="sk-16-h">
<header class="sk-16__head">
<p class="sk-16__eyebrow">thread · 3 shells + 1 reply</p>
<h2 class="sk-16__h" id="sk-16-h">The post is real. The thread is pending.</h2>
<p class="sk-16__sub">Comments load after the article, so the skeleton has to inherit the resolved thread's gaps and indent — otherwise the page re-rhythms when replies land.</p>
</header>
<article class="sk-16__post">
<div class="sk-16__prow">
<img class="sk-16__pic" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=96&h=96&q=70" width="96" height="96" alt="Alex Chen" loading="lazy" decoding="async">
<div class="sk-16__ident">
<p class="sk-16__name">Alex Chen</p>
<p class="sk-16__meta">2 hours ago · 24 replies</p>
</div>
</div>
<p class="sk-16__copy">We replaced every spinner in the review flow with a skeleton that matches its resolved layout. Reviewers stopped reporting “the page jumped” within a week.</p>
</article>
<ul class="sk-16__thread" role="status" aria-live="polite">
<span class="sk-16__vh">Loading 3 comments…</span>
<li class="sk-16__comment" style="--i:0">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:8rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:6.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:92%"></span><span class="sk-16__bar" style="inline-size:58%"></span></div>
</article>
</li>
<li class="sk-16__comment" style="--i:1">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:10rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:5.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:86%"></span><span class="sk-16__bar" style="inline-size:44%"></span></div>
</article>
<ul class="sk-16__replies">
<li class="sk-16__comment" style="--i:2">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar sk-16__avatar--sm"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:7rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:4.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:52%"></span></div>
</article>
</li>
</ul>
</li>
<li class="sk-16__comment" style="--i:3">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:9rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:7rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:90%"></span><span class="sk-16__bar" style="inline-size:62%"></span></div>
</article>
</li>
</ul>
<aside class="sk-16__spec" aria-label="Recipe">
<p class="sk-16__srow2"><span class="sk-16__skey">rhythm</span><span class="sk-16__sval">thread gap 1.35rem · line gap .55rem — identical to the resolved thread</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">indent</span><span class="sk-16__sval">padding-inline-start: clamp(1.5rem, 4vw, 2.75rem) + a 1px rail</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">stagger</span><span class="sk-16__sval">animation-delay: calc(var(--i) * -.22s) per comment, never per bar</span></p>
</aside>
</section>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.sk-16 {
--bg: #07090b;
--bg2: #0e1215;
--surface: rgba(255,255,255,.032);
--line: rgba(255,255,255,.1);
--ink: #f3f6f7;
--muted: rgba(243,246,247,.62);
--faint: rgba(243,246,247,.42);
--accent: #fdba74;
--sk: rgba(243,246,247,.1);
--sheen: rgba(243,246,247,.17);
--mono: 'Geist Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
color: var(--ink);
isolation: isolate;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
background: radial-gradient(56% 40% at 14% -6%, rgba(253,186,116,.1) 0%, transparent 60%), linear-gradient(178deg,var(--bg2) 0%,var(--bg) 62%);
}
.sk-16 *,
.sk-16 *::before,
.sk-16 *::after {
box-sizing: border-box;
}
@supports (color: oklch(0.7 0.1 60)) {
.sk-16 {
--ink: oklch(0.97 0.005 200);
--muted: oklch(0.97 0.005 200 / .62);
--faint: oklch(0.97 0.005 200 / .42);
--accent: oklch(0.82 0.11 65);
}
}
@supports (color: color-mix(in oklab, white 10%, transparent)) {
.sk-16 {
--sk: color-mix(in oklab, currentColor 10%, transparent);
--sheen: color-mix(in oklab, currentColor 17%, transparent);
}
}
.sk-16__vh {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.sk-16__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.15rem,4vw,3.25rem);
}
.sk-16__wrap {
inline-size: min(52rem,100%);
display: grid;
gap: clamp(1rem,2.2vw,1.5rem);
}
.sk-16__head {
display: grid;
gap: .6rem;
}
.sk-16__eyebrow {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
}
.sk-16__h {
margin: 0;
font-size: clamp(1.6rem,3.6vw,2.5rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.06;
text-wrap: balance;
}
.sk-16__sub {
margin: 0;
font-size: clamp(.94rem,1.3vw,1.02rem);
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.sk-16__post {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.4rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__prow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__pic {
inline-size: 2.6rem;
aspect-ratio: 1;
border-radius: 999px;
object-fit: cover;
background: var(--sk);
flex: none;
}
.sk-16__ident {
display: grid;
gap: .45rem;
min-inline-size: 0;
}
.sk-16__name {
margin: 0;
font-size: .98rem;
font-weight: 500;
}
.sk-16__meta {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
color: var(--muted);
}
.sk-16__copy {
margin: 0;
font-size: .95rem;
line-height: 1.62;
color: var(--muted);
max-inline-size: 56ch;
text-wrap: pretty;
}
.sk-16__thread {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 1.35rem;
}
.sk-16__comment {
display: grid;
gap: 1.35rem;
}
.sk-16__shell {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.35rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__srow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__lines {
display: grid;
gap: .55rem;
}
.sk-16__replies {
list-style: none;
margin: 0;
padding-inline-start: clamp(1.5rem,4vw,2.75rem);
border-inline-start: 1px solid var(--line);
display: grid;
gap: 1.35rem;
}
.sk-16__bar {
display: block;
position: relative;
overflow: hidden;
block-size: .72rem;
inline-size: 100%;
border-radius: 6px;
background: var(--sk);
}
.sk-16__bar::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(100deg, transparent 18%, var(--sheen) 50%, transparent 82%);
translate: -100% 0;
animation: sk-16-sheen 1.4s linear infinite;
animation-delay: calc(var(--i,0) * -.22s);
}
@keyframes sk-16-sheen {
to {
translate: 100% 0;
}
}
.sk-16__bar--name {
block-size: .85rem;
}
.sk-16__bar--meta {
block-size: .65rem;
}
.sk-16__avatar {
inline-size: 2.6rem;
block-size: auto;
aspect-ratio: 1;
border-radius: 999px;
flex: none;
}
.sk-16__avatar--sm {
inline-size: 2.1rem;
}
.sk-16__spec {
padding: clamp(.85rem,1.5vw,1.1rem) clamp(1rem,1.8vw,1.3rem);
border: 1px solid var(--line);
border-radius: .9rem;
background: var(--surface);
display: grid;
gap: .45rem;
}
.sk-16__srow2 {
margin: 0;
display: flex;
gap: .9rem;
flex-wrap: wrap;
font-family: var(--mono);
font-size: .74rem;
line-height: 1.5;
}
.sk-16__skey {
min-inline-size: 4.2rem;
color: var(--accent);
letter-spacing: .08em;
text-transform: uppercase;
font-size: .66rem;
padding-block-start: .12rem;
}
.sk-16__sval {
color: var(--muted);
word-break: break-word;
}
.sk-16[data-theme="light"] {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
@media (prefers-color-scheme: light) {
.sk-16:not([data-theme="dark"]) {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
}
@media (prefers-reduced-motion: reduce) {
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__bar {
background: linear-gradient(105deg, var(--sk) 0%, var(--sheen) 50%, var(--sk) 100%);
}
}
@media (forced-colors: active) {
.sk-16__bar {
background: GrayText;
forced-color-adjust: none;
}
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__shell,
.sk-16__post,
.sk-16__spec {
border-color: CanvasText;
}
}@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.sk-16 {
--bg: #07090b;
--bg2: #0e1215;
--surface: rgba(255,255,255,.032);
--line: rgba(255,255,255,.1);
--ink: #f3f6f7;
--muted: rgba(243,246,247,.62);
--faint: rgba(243,246,247,.42);
--accent: #fdba74;
--sk: rgba(243,246,247,.1);
--sheen: rgba(243,246,247,.17);
--mono: 'Geist Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
color: var(--ink);
isolation: isolate;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
background: radial-gradient(56% 40% at 14% -6%, rgba(253,186,116,.1) 0%, transparent 60%), linear-gradient(178deg,var(--bg2) 0%,var(--bg) 62%);
}
.sk-16 *,
.sk-16 *::before,
.sk-16 *::after {
box-sizing: border-box;
}
@supports (color: oklch(0.7 0.1 60)) {
.sk-16 {
--ink: oklch(0.97 0.005 200);
--muted: oklch(0.97 0.005 200 / .62);
--faint: oklch(0.97 0.005 200 / .42);
--accent: oklch(0.82 0.11 65);
}
}
@supports (color: color-mix(in oklab, white 10%, transparent)) {
.sk-16 {
--sk: color-mix(in oklab, currentColor 10%, transparent);
--sheen: color-mix(in oklab, currentColor 17%, transparent);
}
}
.sk-16__vh {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.sk-16__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.15rem,4vw,3.25rem);
}
.sk-16__wrap {
inline-size: min(52rem,100%);
display: grid;
gap: clamp(1rem,2.2vw,1.5rem);
}
.sk-16__head {
display: grid;
gap: .6rem;
}
.sk-16__eyebrow {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
}
.sk-16__h {
margin: 0;
font-size: clamp(1.6rem,3.6vw,2.5rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.06;
text-wrap: balance;
}
.sk-16__sub {
margin: 0;
font-size: clamp(.94rem,1.3vw,1.02rem);
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.sk-16__post {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.4rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__prow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__pic {
inline-size: 2.6rem;
aspect-ratio: 1;
border-radius: 999px;
object-fit: cover;
background: var(--sk);
flex: none;
}
.sk-16__ident {
display: grid;
gap: .45rem;
min-inline-size: 0;
}
.sk-16__name {
margin: 0;
font-size: .98rem;
font-weight: 500;
}
.sk-16__meta {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
color: var(--muted);
}
.sk-16__copy {
margin: 0;
font-size: .95rem;
line-height: 1.62;
color: var(--muted);
max-inline-size: 56ch;
text-wrap: pretty;
}
.sk-16__thread {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 1.35rem;
}
.sk-16__comment {
display: grid;
gap: 1.35rem;
}
.sk-16__shell {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.35rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__srow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__lines {
display: grid;
gap: .55rem;
}
.sk-16__replies {
list-style: none;
margin: 0;
padding-inline-start: clamp(1.5rem,4vw,2.75rem);
border-inline-start: 1px solid var(--line);
display: grid;
gap: 1.35rem;
}
.sk-16__bar {
display: block;
position: relative;
overflow: hidden;
block-size: .72rem;
inline-size: 100%;
border-radius: 6px;
background: var(--sk);
}
.sk-16__bar::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(100deg, transparent 18%, var(--sheen) 50%, transparent 82%);
translate: -100% 0;
animation: sk-16-sheen 1.4s linear infinite;
animation-delay: calc(var(--i,0) * -.22s);
}
@keyframes sk-16-sheen {
to {
translate: 100% 0;
}
}
.sk-16__bar--name {
block-size: .85rem;
}
.sk-16__bar--meta {
block-size: .65rem;
}
.sk-16__avatar {
inline-size: 2.6rem;
block-size: auto;
aspect-ratio: 1;
border-radius: 999px;
flex: none;
}
.sk-16__avatar--sm {
inline-size: 2.1rem;
}
.sk-16__spec {
padding: clamp(.85rem,1.5vw,1.1rem) clamp(1rem,1.8vw,1.3rem);
border: 1px solid var(--line);
border-radius: .9rem;
background: var(--surface);
display: grid;
gap: .45rem;
}
.sk-16__srow2 {
margin: 0;
display: flex;
gap: .9rem;
flex-wrap: wrap;
font-family: var(--mono);
font-size: .74rem;
line-height: 1.5;
}
.sk-16__skey {
min-inline-size: 4.2rem;
color: var(--accent);
letter-spacing: .08em;
text-transform: uppercase;
font-size: .66rem;
padding-block-start: .12rem;
}
.sk-16__sval {
color: var(--muted);
word-break: break-word;
}
.sk-16[data-theme="light"] {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
@media (prefers-color-scheme: light) {
.sk-16:not([data-theme="dark"]) {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
}
@media (prefers-reduced-motion: reduce) {
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__bar {
background: linear-gradient(105deg, var(--sk) 0%, var(--sheen) 50%, var(--sk) 100%);
}
}
@media (forced-colors: active) {
.sk-16__bar {
background: GrayText;
forced-color-adjust: none;
}
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__shell,
.sk-16__post,
.sk-16__spec {
border-color: CanvasText;
}
}Here's a working CSS Skeleton Loader from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Comment Thread Skeleton Loader
Source: https://codefronts.com/components/css-skeleton-loaders/comment-thread/
Threads load after the post, so the skeleton sits under real content and has to match its rhythm exactly. Three comment shells — avatar, name, meta, three text lines — plus one indented reply, all built from a real <ul> with the same indent and vertical spacing the resolved thread uses.
## HTML
```html
<div class="sk-16">
<div class="sk-16__stage">
<section class="sk-16__wrap" aria-labelledby="sk-16-h">
<header class="sk-16__head">
<p class="sk-16__eyebrow">thread · 3 shells + 1 reply</p>
<h2 class="sk-16__h" id="sk-16-h">The post is real. The thread is pending.</h2>
<p class="sk-16__sub">Comments load after the article, so the skeleton has to inherit the resolved thread's gaps and indent — otherwise the page re-rhythms when replies land.</p>
</header>
<article class="sk-16__post">
<div class="sk-16__prow">
<img class="sk-16__pic" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=96&h=96&q=70" width="96" height="96" alt="Alex Chen" loading="lazy" decoding="async">
<div class="sk-16__ident">
<p class="sk-16__name">Alex Chen</p>
<p class="sk-16__meta">2 hours ago · 24 replies</p>
</div>
</div>
<p class="sk-16__copy">We replaced every spinner in the review flow with a skeleton that matches its resolved layout. Reviewers stopped reporting “the page jumped” within a week.</p>
</article>
<ul class="sk-16__thread" role="status" aria-live="polite">
<span class="sk-16__vh">Loading 3 comments…</span>
<li class="sk-16__comment" style="--i:0">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:8rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:6.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:92%"></span><span class="sk-16__bar" style="inline-size:58%"></span></div>
</article>
</li>
<li class="sk-16__comment" style="--i:1">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:10rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:5.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:86%"></span><span class="sk-16__bar" style="inline-size:44%"></span></div>
</article>
<ul class="sk-16__replies">
<li class="sk-16__comment" style="--i:2">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar sk-16__avatar--sm"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:7rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:4.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:52%"></span></div>
</article>
</li>
</ul>
</li>
<li class="sk-16__comment" style="--i:3">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:9rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:7rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:90%"></span><span class="sk-16__bar" style="inline-size:62%"></span></div>
</article>
</li>
</ul>
<aside class="sk-16__spec" aria-label="Recipe">
<p class="sk-16__srow2"><span class="sk-16__skey">rhythm</span><span class="sk-16__sval">thread gap 1.35rem · line gap .55rem — identical to the resolved thread</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">indent</span><span class="sk-16__sval">padding-inline-start: clamp(1.5rem, 4vw, 2.75rem) + a 1px rail</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">stagger</span><span class="sk-16__sval">animation-delay: calc(var(--i) * -.22s) per comment, never per bar</span></p>
</aside>
</section>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.sk-16 {
--bg: #07090b;
--bg2: #0e1215;
--surface: rgba(255,255,255,.032);
--line: rgba(255,255,255,.1);
--ink: #f3f6f7;
--muted: rgba(243,246,247,.62);
--faint: rgba(243,246,247,.42);
--accent: #fdba74;
--sk: rgba(243,246,247,.1);
--sheen: rgba(243,246,247,.17);
--mono: 'Geist Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
color: var(--ink);
isolation: isolate;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
background: radial-gradient(56% 40% at 14% -6%, rgba(253,186,116,.1) 0%, transparent 60%), linear-gradient(178deg,var(--bg2) 0%,var(--bg) 62%);
}
.sk-16 *,
.sk-16 *::before,
.sk-16 *::after {
box-sizing: border-box;
}
@supports (color: oklch(0.7 0.1 60)) {
.sk-16 {
--ink: oklch(0.97 0.005 200);
--muted: oklch(0.97 0.005 200 / .62);
--faint: oklch(0.97 0.005 200 / .42);
--accent: oklch(0.82 0.11 65);
}
}
@supports (color: color-mix(in oklab, white 10%, transparent)) {
.sk-16 {
--sk: color-mix(in oklab, currentColor 10%, transparent);
--sheen: color-mix(in oklab, currentColor 17%, transparent);
}
}
.sk-16__vh {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.sk-16__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.15rem,4vw,3.25rem);
}
.sk-16__wrap {
inline-size: min(52rem,100%);
display: grid;
gap: clamp(1rem,2.2vw,1.5rem);
}
.sk-16__head {
display: grid;
gap: .6rem;
}
.sk-16__eyebrow {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
}
.sk-16__h {
margin: 0;
font-size: clamp(1.6rem,3.6vw,2.5rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.06;
text-wrap: balance;
}
.sk-16__sub {
margin: 0;
font-size: clamp(.94rem,1.3vw,1.02rem);
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.sk-16__post {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.4rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__prow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__pic {
inline-size: 2.6rem;
aspect-ratio: 1;
border-radius: 999px;
object-fit: cover;
background: var(--sk);
flex: none;
}
.sk-16__ident {
display: grid;
gap: .45rem;
min-inline-size: 0;
}
.sk-16__name {
margin: 0;
font-size: .98rem;
font-weight: 500;
}
.sk-16__meta {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
color: var(--muted);
}
.sk-16__copy {
margin: 0;
font-size: .95rem;
line-height: 1.62;
color: var(--muted);
max-inline-size: 56ch;
text-wrap: pretty;
}
.sk-16__thread {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 1.35rem;
}
.sk-16__comment {
display: grid;
gap: 1.35rem;
}
.sk-16__shell {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.35rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__srow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__lines {
display: grid;
gap: .55rem;
}
.sk-16__replies {
list-style: none;
margin: 0;
padding-inline-start: clamp(1.5rem,4vw,2.75rem);
border-inline-start: 1px solid var(--line);
display: grid;
gap: 1.35rem;
}
.sk-16__bar {
display: block;
position: relative;
overflow: hidden;
block-size: .72rem;
inline-size: 100%;
border-radius: 6px;
background: var(--sk);
}
.sk-16__bar::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(100deg, transparent 18%, var(--sheen) 50%, transparent 82%);
translate: -100% 0;
animation: sk-16-sheen 1.4s linear infinite;
animation-delay: calc(var(--i,0) * -.22s);
}
@keyframes sk-16-sheen {
to {
translate: 100% 0;
}
}
.sk-16__bar--name {
block-size: .85rem;
}
.sk-16__bar--meta {
block-size: .65rem;
}
.sk-16__avatar {
inline-size: 2.6rem;
block-size: auto;
aspect-ratio: 1;
border-radius: 999px;
flex: none;
}
.sk-16__avatar--sm {
inline-size: 2.1rem;
}
.sk-16__spec {
padding: clamp(.85rem,1.5vw,1.1rem) clamp(1rem,1.8vw,1.3rem);
border: 1px solid var(--line);
border-radius: .9rem;
background: var(--surface);
display: grid;
gap: .45rem;
}
.sk-16__srow2 {
margin: 0;
display: flex;
gap: .9rem;
flex-wrap: wrap;
font-family: var(--mono);
font-size: .74rem;
line-height: 1.5;
}
.sk-16__skey {
min-inline-size: 4.2rem;
color: var(--accent);
letter-spacing: .08em;
text-transform: uppercase;
font-size: .66rem;
padding-block-start: .12rem;
}
.sk-16__sval {
color: var(--muted);
word-break: break-word;
}
.sk-16[data-theme="light"] {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
@media (prefers-color-scheme: light) {
.sk-16:not([data-theme="dark"]) {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
}
@media (prefers-reduced-motion: reduce) {
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__bar {
background: linear-gradient(105deg, var(--sk) 0%, var(--sheen) 50%, var(--sk) 100%);
}
}
@media (forced-colors: active) {
.sk-16__bar {
background: GrayText;
forced-color-adjust: none;
}
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__shell,
.sk-16__post,
.sk-16__spec {
border-color: CanvasText;
}
}
```Here's a working CSS Skeleton Loader from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Comment Thread Skeleton Loader
Source: https://codefronts.com/components/css-skeleton-loaders/comment-thread/
Threads load after the post, so the skeleton sits under real content and has to match its rhythm exactly. Three comment shells — avatar, name, meta, three text lines — plus one indented reply, all built from a real <ul> with the same indent and vertical spacing the resolved thread uses.
## HTML
```html
<div class="sk-16">
<div class="sk-16__stage">
<section class="sk-16__wrap" aria-labelledby="sk-16-h">
<header class="sk-16__head">
<p class="sk-16__eyebrow">thread · 3 shells + 1 reply</p>
<h2 class="sk-16__h" id="sk-16-h">The post is real. The thread is pending.</h2>
<p class="sk-16__sub">Comments load after the article, so the skeleton has to inherit the resolved thread's gaps and indent — otherwise the page re-rhythms when replies land.</p>
</header>
<article class="sk-16__post">
<div class="sk-16__prow">
<img class="sk-16__pic" src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=96&h=96&q=70" width="96" height="96" alt="Alex Chen" loading="lazy" decoding="async">
<div class="sk-16__ident">
<p class="sk-16__name">Alex Chen</p>
<p class="sk-16__meta">2 hours ago · 24 replies</p>
</div>
</div>
<p class="sk-16__copy">We replaced every spinner in the review flow with a skeleton that matches its resolved layout. Reviewers stopped reporting “the page jumped” within a week.</p>
</article>
<ul class="sk-16__thread" role="status" aria-live="polite">
<span class="sk-16__vh">Loading 3 comments…</span>
<li class="sk-16__comment" style="--i:0">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:8rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:6.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:92%"></span><span class="sk-16__bar" style="inline-size:58%"></span></div>
</article>
</li>
<li class="sk-16__comment" style="--i:1">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:10rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:5.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:86%"></span><span class="sk-16__bar" style="inline-size:44%"></span></div>
</article>
<ul class="sk-16__replies">
<li class="sk-16__comment" style="--i:2">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar sk-16__avatar--sm"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:7rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:4.5rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:52%"></span></div>
</article>
</li>
</ul>
</li>
<li class="sk-16__comment" style="--i:3">
<article class="sk-16__shell" aria-hidden="true">
<div class="sk-16__srow">
<span class="sk-16__bar sk-16__avatar"></span>
<div class="sk-16__ident"><span class="sk-16__bar sk-16__bar--name" style="inline-size:9rem"></span><span class="sk-16__bar sk-16__bar--meta" style="inline-size:7rem"></span></div>
</div>
<div class="sk-16__lines"><span class="sk-16__bar"></span><span class="sk-16__bar" style="inline-size:90%"></span><span class="sk-16__bar" style="inline-size:62%"></span></div>
</article>
</li>
</ul>
<aside class="sk-16__spec" aria-label="Recipe">
<p class="sk-16__srow2"><span class="sk-16__skey">rhythm</span><span class="sk-16__sval">thread gap 1.35rem · line gap .55rem — identical to the resolved thread</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">indent</span><span class="sk-16__sval">padding-inline-start: clamp(1.5rem, 4vw, 2.75rem) + a 1px rail</span></p>
<p class="sk-16__srow2"><span class="sk-16__skey">stagger</span><span class="sk-16__sval">animation-delay: calc(var(--i) * -.22s) per comment, never per bar</span></p>
</aside>
</section>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.sk-16 {
--bg: #07090b;
--bg2: #0e1215;
--surface: rgba(255,255,255,.032);
--line: rgba(255,255,255,.1);
--ink: #f3f6f7;
--muted: rgba(243,246,247,.62);
--faint: rgba(243,246,247,.42);
--accent: #fdba74;
--sk: rgba(243,246,247,.1);
--sheen: rgba(243,246,247,.17);
--mono: 'Geist Mono',ui-monospace,'SFMono-Regular',Menlo,monospace;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
position: relative;
box-sizing: border-box;
color: var(--ink);
isolation: isolate;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
background: radial-gradient(56% 40% at 14% -6%, rgba(253,186,116,.1) 0%, transparent 60%), linear-gradient(178deg,var(--bg2) 0%,var(--bg) 62%);
}
.sk-16 *,
.sk-16 *::before,
.sk-16 *::after {
box-sizing: border-box;
}
@supports (color: oklch(0.7 0.1 60)) {
.sk-16 {
--ink: oklch(0.97 0.005 200);
--muted: oklch(0.97 0.005 200 / .62);
--faint: oklch(0.97 0.005 200 / .42);
--accent: oklch(0.82 0.11 65);
}
}
@supports (color: color-mix(in oklab, white 10%, transparent)) {
.sk-16 {
--sk: color-mix(in oklab, currentColor 10%, transparent);
--sheen: color-mix(in oklab, currentColor 17%, transparent);
}
}
.sk-16__vh {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.sk-16__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.15rem,4vw,3.25rem);
}
.sk-16__wrap {
inline-size: min(52rem,100%);
display: grid;
gap: clamp(1rem,2.2vw,1.5rem);
}
.sk-16__head {
display: grid;
gap: .6rem;
}
.sk-16__eyebrow {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
}
.sk-16__h {
margin: 0;
font-size: clamp(1.6rem,3.6vw,2.5rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.06;
text-wrap: balance;
}
.sk-16__sub {
margin: 0;
font-size: clamp(.94rem,1.3vw,1.02rem);
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.sk-16__post {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.4rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__prow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__pic {
inline-size: 2.6rem;
aspect-ratio: 1;
border-radius: 999px;
object-fit: cover;
background: var(--sk);
flex: none;
}
.sk-16__ident {
display: grid;
gap: .45rem;
min-inline-size: 0;
}
.sk-16__name {
margin: 0;
font-size: .98rem;
font-weight: 500;
}
.sk-16__meta {
margin: 0;
font-family: var(--mono);
font-size: .72rem;
color: var(--muted);
}
.sk-16__copy {
margin: 0;
font-size: .95rem;
line-height: 1.62;
color: var(--muted);
max-inline-size: 56ch;
text-wrap: pretty;
}
.sk-16__thread {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 1.35rem;
}
.sk-16__comment {
display: grid;
gap: 1.35rem;
}
.sk-16__shell {
display: grid;
gap: .8rem;
padding: clamp(1rem,1.9vw,1.35rem);
border: 1px solid var(--line);
border-radius: 1.05rem;
background: var(--surface);
}
.sk-16__srow {
display: flex;
align-items: center;
gap: .9rem;
}
.sk-16__lines {
display: grid;
gap: .55rem;
}
.sk-16__replies {
list-style: none;
margin: 0;
padding-inline-start: clamp(1.5rem,4vw,2.75rem);
border-inline-start: 1px solid var(--line);
display: grid;
gap: 1.35rem;
}
.sk-16__bar {
display: block;
position: relative;
overflow: hidden;
block-size: .72rem;
inline-size: 100%;
border-radius: 6px;
background: var(--sk);
}
.sk-16__bar::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(100deg, transparent 18%, var(--sheen) 50%, transparent 82%);
translate: -100% 0;
animation: sk-16-sheen 1.4s linear infinite;
animation-delay: calc(var(--i,0) * -.22s);
}
@keyframes sk-16-sheen {
to {
translate: 100% 0;
}
}
.sk-16__bar--name {
block-size: .85rem;
}
.sk-16__bar--meta {
block-size: .65rem;
}
.sk-16__avatar {
inline-size: 2.6rem;
block-size: auto;
aspect-ratio: 1;
border-radius: 999px;
flex: none;
}
.sk-16__avatar--sm {
inline-size: 2.1rem;
}
.sk-16__spec {
padding: clamp(.85rem,1.5vw,1.1rem) clamp(1rem,1.8vw,1.3rem);
border: 1px solid var(--line);
border-radius: .9rem;
background: var(--surface);
display: grid;
gap: .45rem;
}
.sk-16__srow2 {
margin: 0;
display: flex;
gap: .9rem;
flex-wrap: wrap;
font-family: var(--mono);
font-size: .74rem;
line-height: 1.5;
}
.sk-16__skey {
min-inline-size: 4.2rem;
color: var(--accent);
letter-spacing: .08em;
text-transform: uppercase;
font-size: .66rem;
padding-block-start: .12rem;
}
.sk-16__sval {
color: var(--muted);
word-break: break-word;
}
.sk-16[data-theme="light"] {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
@media (prefers-color-scheme: light) {
.sk-16:not([data-theme="dark"]) {
--bg: #faf9f7;
--bg2: #fff;
--surface: rgba(12,14,16,.026);
--line: rgba(12,14,16,.11);
--ink: #0c0e10;
--muted: rgba(12,14,16,.62);
--faint: rgba(12,14,16,.44);
--accent: #b45309;
background: linear-gradient(178deg,#fff 0%,#f4f1ec 100%);
}
}
@media (prefers-reduced-motion: reduce) {
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__bar {
background: linear-gradient(105deg, var(--sk) 0%, var(--sheen) 50%, var(--sk) 100%);
}
}
@media (forced-colors: active) {
.sk-16__bar {
background: GrayText;
forced-color-adjust: none;
}
.sk-16__bar::after {
animation: none;
background: none;
}
.sk-16__shell,
.sk-16__post,
.sk-16__spec {
border-color: CanvasText;
}
}
```How this works
The post is real; only the thread is pending. That is the actual production case, and it changes the requirement: the skeleton has to inherit the resolved thread's spacing, or the page visibly re-rhythms when replies land. Share the numbers rather than eyeballing them:
.sk-16__thread{ display:grid; gap:1.35rem } /* between comments */
.sk-16__lines { display:grid; gap:.55rem } /* between text lines */
.sk-16__replies{ padding-inline-start:clamp(1.5rem,4vw,2.75rem);
border-inline-start:1px solid var(--line) } /* the rail */Semantics survive the loading state. A thread is a list, so it stays a list while pending. Screen reader users then hear "list, 3 items" and know the shape of what is coming — a stack of divs tells them nothing:
<ul class="sk-16__thread" role="status" aria-live="polite">
<li><article aria-hidden="true">…bars…</article></li>
<li><article aria-hidden="true">…bars…</article>
<ul class="sk-16__replies"><li>…</li></ul>
</li>
</ul>Three lines, descending, last one short. Comments are shorter than articles — three lines at 100%, 92% and 58% reads as a real comment; five equal-length lines reads as a paragraph placeholder that will visibly shrink on resolve.
Stagger by comment, not by bar. One --i per comment offsets the whole shell's sheen, so the thread appears to fill downward. Per-bar stagger inside a comment looks like a glitch:
.sk-16__comment{ --i:0 } .sk-16__comment:nth-child(2){ --i:1 } …
.sk-16__bar::after{ animation-delay:calc(var(--i) * -.22s) }Make it yours
- Match your comment count: two shells for a quiet thread, five for a busy one — never more than fits the first viewport.
- Indent replies with the same value your resolved thread uses; the rail makes an off-by-one obvious.
- Add a vote-control placeholder (two small bars stacked) on the left for Reddit-style threads.
- Include a meta row placeholder (timestamp, reply link) if your real comments show one.
- For chat rather than threads, alternate alignment and drop the rail.
- Skip the avatar entirely for anonymous or handle-only systems — the two text lines carry the row.
Gotchas — read before shipping
- A skeleton indent that differs from the resolved indent makes the whole thread slide sideways on load — share the token.
- Do not
aria-hiddenthe list itself, only the decorative shells inside it; hiding the list removes the item-count announcement. - Five identical-length lines per comment is the most common tell of a lazy skeleton. Vary them.
- Nesting more than one reply level in a skeleton is noise — users cannot verify depth they cannot read.
- Announcing per comment produces three overlapping messages. One status region for the thread.
- If replies load lazily on scroll, reserve their height too, or the scroll position jumps under the user's thumb.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 88+.
Grid gap, logical properties and aspect-ratio are baseline (Chrome 88 / Safari 15 / Firefox 89). Custom properties in calc() for the stagger are universal. color-mix() is gated behind @supports with an rgba() fallback declared first.