Claude Code Skeleton Loading Guide: React, CLS, and Accessible States
Build accessible skeleton loading with Claude Code: React example, CLS guardrails, pitfalls, and checks.
Skeleton loading is the UI pattern where you show the rough shape of the page while data is still loading. In plain terms, it reserves the seats for the image, title, description, and actions before the real content arrives.
A spinner only says “something is happening.” A skeleton says “this is the kind of content that will appear here.” If the image, ad slot, or API response lands inside a space that was already reserved, the page is less likely to jump. That visual stability connects directly to web.dev’s guidance on Cumulative Layout Shift and Core Web Vitals.
This guide shows how to ask Claude Code for a practical skeleton loading implementation, then reviews copy-pasteable React and CSS, accessibility notes, failure cases, and lightweight checks. For related ClaudeCodeLab context, pair it with performance optimization, image lazy loading, and the accessibility workflow.
Break Down The Job First
A skeleton is not just a gray rectangle. A real feature needs loading, success, empty, and error states, and those states need to share a stable layout. If you ask Claude Code for “a nice skeleton,” it may produce a polished shimmer while missing error handling, reduced-motion behavior, or screen reader messaging.
flowchart LR
P["Claude Code prompt"] --> S["same-size skeleton"]
S --> D["loaded data"]
D --> E["empty state"]
D --> X["error state"]
S --> A["aria-busy / status"]
S --> M["prefers-reduced-motion"]
S --> C["CLS check"]
Start with a prompt that defines scope and verification:
Read the existing card/list components before editing.
Implement skeleton loading only for the article cards list.
Keep the skeleton dimensions close to the loaded content.
Handle loading, empty, error, and success states.
Respect prefers-reduced-motion and avoid layout shift.
Add a small Playwright check if the project already uses Playwright.
Do not change unrelated styles, routing, or data fetching.
prefers-reduced-motion means the user has asked the operating system or browser to reduce motion. Strong shimmer effects can be uncomfortable, so check MDN’s prefers-reduced-motion reference and provide a still fallback.
Practical Use Cases
Skeleton loading is most useful when the reader can understand what is coming, but the data cannot be shown yet.
| Use case | What to reserve | Watch out for |
|---|---|---|
| Article card lists | Thumbnail, two-line title, summary, tag | Keep media height fixed so cards do not snap shorter or taller |
| Dashboards | KPI cards, chart frames, recent activity | Do not reveal misleading partial numbers before the full state is ready |
| Ecommerce grids | Product image, name, price, rating | Avoid showing stale price or stock while the request is refreshing |
| Admin tables | Header, rows, action area | If row counts change wildly, review pagination and filtering too |
| Consulting or content LPs | Proof cards, CTA, FAQ slots | Late CTAs can push the conversion path below the reader’s eye line |
In a ClaudeCodeLab consulting funnel, the biggest improvement came from reserving the article card and CTA space before the data arrived. The mistake was making the skeleton taller than the finished content. The page looked calm while loading, then jumped upward when the real text rendered. A skeleton is not decoration; it is a layout contract.
Copy-Paste React Example
Paste this into src/App.tsx in a Vite + React + TypeScript project. It simulates latency with setTimeout and lets you switch between success, empty, and error states. This is also the level of state detail I prefer to give Claude Code before it edits a real codebase.
import { useEffect, useState } from "react";
import "./skeleton-demo.css";
type Article = {
id: number;
title: string;
description: string;
tag: string;
};
type LoadState = "loading" | "success" | "empty" | "error";
const demoArticles: Article[] = [
{
id: 1,
title: "Ship safer UI diffs with Claude Code",
description: "Read the existing components first, then improve only the loading experience.",
tag: "UX",
},
{
id: 2,
title: "Reserve image space without increasing CLS",
description: "Fix media, title, and summary heights before the real data arrives.",
tag: "Performance",
},
{
id: 3,
title: "Make loading states accessible",
description: "Combine aria-busy, status messages, and reduced-motion behavior.",
tag: "A11y",
},
];
function SkeletonLine({ width = "100%" }: { width?: string }) {
return <span className="sk-line" style={{ width }} aria-hidden="true" />;
}
function ArticleCardSkeleton() {
return (
<article className="article-card is-skeleton" aria-hidden="true">
<div className="sk-media" />
<div className="article-card__body">
<SkeletonLine width="46%" />
<SkeletonLine />
<SkeletonLine width="86%" />
<SkeletonLine width="32%" />
</div>
</article>
);
}
function ArticleCard({ article }: { article: Article }) {
return (
<article className="article-card">
<div className="article-card__media">{article.tag}</div>
<div className="article-card__body">
<p className="article-card__tag">{article.tag}</p>
<h2>{article.title}</h2>
<p>{article.description}</p>
</div>
</article>
);
}
export default function App() {
const [state, setState] = useState<LoadState>("loading");
const [articles, setArticles] = useState<Article[]>([]);
useEffect(() => {
const timer = window.setTimeout(() => {
setArticles(demoArticles);
setState("success");
}, 1200);
return () => window.clearTimeout(timer);
}, []);
const reloadAs = (nextState: LoadState) => {
setState("loading");
setArticles([]);
window.setTimeout(() => {
setArticles(nextState === "success" ? demoArticles : []);
setState(nextState);
}, 700);
};
return (
<main className="demo-shell">
<div className="demo-toolbar" aria-label="Change visible state">
<button onClick={() => reloadAs("success")}>Success</button>
<button onClick={() => reloadAs("empty")}>Empty</button>
<button onClick={() => reloadAs("error")}>Error</button>
</div>
<section
aria-busy={state === "loading"}
aria-describedby="article-list-status"
className="article-grid"
>
<p id="article-list-status" className="sr-only" role="status">
{state === "loading" ? "Loading article list" : "Article list loaded"}
</p>
{state === "loading" &&
Array.from({ length: 3 }).map((_, index) => (
<ArticleCardSkeleton key={index} />
))}
{state === "success" &&
articles.map((article) => (
<ArticleCard key={article.id} article={article} />
))}
{state === "empty" && (
<div className="state-panel">No matching articles yet.</div>
)}
{state === "error" && (
<div className="state-panel" role="alert">
The article list could not be loaded. Please try again later.
</div>
)}
</section>
</main>
);
}
The accessibility detail that matters most is not over-announcing the skeleton itself. A screen reader does not need to hear that there are three gray bars. In this example the skeleton cards are aria-hidden, while one quiet role="status" message describes the list state. MDN’s ARIA status role is the useful reference for non-urgent state updates.
CSS For Stable Size And Motion
Save this as src/skeleton-demo.css. The important part is keeping min-height, media height, and body spacing close between the loading and loaded states. The shimmer is restrained, and it stops for users who prefer reduced motion.
:root {
color: #18212f;
background: #f6f7f9;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button {
min-height: 40px;
border: 1px solid #b8c2d6;
border-radius: 8px;
background: #ffffff;
color: #18212f;
padding: 0 14px;
font-weight: 700;
}
.demo-shell {
width: min(1040px, calc(100% - 32px));
margin: 40px auto;
}
.demo-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 18px;
}
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.article-card {
min-height: 316px;
overflow: hidden;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
}
.article-card__media,
.sk-media {
display: grid;
min-height: 148px;
place-items: center;
background: #dfe7f3;
color: #39506f;
font-weight: 800;
}
.article-card__body {
display: grid;
gap: 10px;
padding: 18px;
}
.article-card__tag {
color: #3b6b4f;
font-size: 0.875rem;
font-weight: 800;
}
.article-card h2 {
min-height: 56px;
margin: 0;
font-size: 1.16rem;
line-height: 1.45;
}
.article-card p {
margin: 0;
line-height: 1.7;
}
.sk-line,
.sk-media {
border-radius: 8px;
background: linear-gradient(90deg, #d9e0ea 25%, #edf1f7 37%, #d9e0ea 63%);
background-size: 240% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.sk-line {
display: block;
height: 16px;
}
.state-panel {
min-height: 180px;
display: grid;
place-items: center;
border: 1px solid #d7deea;
border-radius: 8px;
background: #ffffff;
padding: 24px;
text-align: center;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
@keyframes skeleton-shimmer {
from {
background-position: 120% 0;
}
to {
background-position: -120% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.sk-line,
.sk-media {
animation: none;
background: #d9e0ea;
}
}
This CSS prioritizes stable layout over a dramatic shine. If skeleton line widths are too random, the final content will feel like it is snapping into a different design. Reserve a two-line title, a two-line summary, and a predictable media area instead.
A Small Playwright Check
If the project already uses Playwright, add one focused check. This does not prove real-world CLS is perfect, but it catches obvious regressions before review.
import { expect, test } from "@playwright/test";
test("article skeleton keeps a stable card area", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Loading article list")).toBeAttached();
await expect(page.locator(".is-skeleton")).toHaveCount(3);
const firstBox = await page.locator(".article-card").first().boundingBox();
expect(firstBox?.height).toBeGreaterThan(280);
await page.getByRole("button", { name: "Error" }).click();
await expect(page.getByRole("alert")).toContainText("could not be loaded");
});
Real CLS still depends on field conditions: images, ad slots, fonts, third-party scripts, network timing, and user devices. Treat this test as an early tripwire, then use the web.dev CLS guidance and production data for stronger evidence.
Common Pitfalls
The first pitfall is a skeleton that does not match the final content. It looks polished during loading, then the real card stretches or collapses. Reserve image dimensions with width, height, or aspect-ratio, and reserve space for ads and CTAs before they render.
The second pitfall is showing a skeleton for every fast request. If the request usually finishes in 100ms, the skeleton may create flicker. In production, use rules such as “show after 300ms,” “show only on first load,” or “keep previous data visible during refresh.”
The third pitfall is too much accessibility noise. If every card has its own role="status", assistive technology may announce repeated messages. Keep the live message at the list level and hide the purely visual skeleton shapes.
The fourth pitfall is forgetting the failure path. If an API fails and the skeleton stays forever, the reader believes the page is still working. Design error, empty, and retry states separately.
The fifth pitfall is asking Claude Code to decide the product priority. Claude Code can read files and generate a diff, but a human still decides which CTA must remain visible, how much ad space to reserve, and what content is safe to show first.
Claude Code Review Prompt
After implementation, switch Claude Code into review mode:
Review only the skeleton loading changes.
Check whether loaded content and skeleton content reserve similar space.
Check loading, success, empty, and error states.
Check reduced-motion behavior and ARIA announcements.
Point out any code that may increase CLS or create repeated screen reader messages.
Return findings with file names and exact lines.
This turns Claude Code from implementer into critic. For article sites, ads, related posts, CTAs, and lazy images all affect the same visual flow. Use the CSS styling guide and testing strategies to separate visual checks, screen reader checks, and regression tests.
Monetization Angle
Skeleton loading also protects revenue paths. If a consulting CTA, product card, newsletter form, or ad slot appears late and pushes the article down, readers lose their place or click the wrong area. That hurts trust and conversion.
Individual builders can start with the free Claude Code cheatsheet to standardize review steps. Teams that want skeleton loading, lazy images, Core Web Vitals, and accessibility turned into a shared engineering workflow can use Claude Code training and consultation. Masa can review the existing repository and turn the improvements into practical prompts, components, and checks.
Summary
Good skeleton loading is not a trick for hiding waiting time. It reserves space close to the final screen, lowers uncertainty, and reduces obvious layout movement. When using Claude Code, give it the target component, states, dimensions, accessibility requirements, and verification commands at the same time.
After trying this workflow, the most useful changes were fixing media and title height first, announcing loading at the list level, and stopping animation for reduced-motion users. When I focused only on the shimmer effect, empty and error states were left for later and review took longer. Ask Claude Code for implementation and verification together.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.