React
Skeleton Loader
SVG
Performance
UI Patterns
React Content Loader: The Complete Guide to SVG Skeleton Screens
Loading states are one of those UI details that separate a polished product from a rough prototype.
A blank white screen while data fetches is, at best, confusing — at worst, it sends users straight to the back button.
react-content-loader
solves this with animated SVG skeleton screens that mirror your actual content layout,
keeping users anchored in the experience while the real data arrives.
This guide covers everything from installation and basic setup
through advanced customization patterns —
with enough working code that you can ship something today.
What Is react-content-loader and Why Should You Care?
At its core, react-content-loader is a lightweight
React loading library
that renders SVG-based animated placeholders.
The shimmer effect — that left-to-right gradient sweep — is built in.
You define shapes. The library does the animation. It’s a clean division of labour.
The underlying mechanism is pure SVG: a <linearGradient> animated with a
CSS @keyframes rule drives the shimmer across whatever shapes you’ve drawn inside the
ContentLoader wrapper. Because it’s SVG, the placeholder can be pixel-perfect —
matching exact border radii, column widths, avatar proportions — without writing a line of custom CSS.
That’s the reason it outclasses generic CSS-based shimmer hacks for any layout with structural complexity.
From a UX standpoint, skeleton screens measurably improve perceived performance.
Research published by the Nielsen Norman Group and replicated in various A/B tests consistently shows
that users tolerate waiting longer when progress is visible and structured.
A skeleton that resembles a card grid feels faster than a spinner sitting in empty space,
even when both resolve at the same millisecond. That’s not magic — it’s expectation management,
and React placeholder loading patterns do it better than almost anything else in the toolkit.
Installation and Project Setup
Getting react-content-loader into your project takes about ninety seconds.
The package has a single peer dependency — React itself — and ships with TypeScript definitions out of the box,
so there’s no @types/ package to chase down. Install it with npm or yarn:
# npm
npm install react-content-loader
# yarn
yarn add react-content-loader
# pnpm
pnpm add react-content-loader
Once installed, import ContentLoader and the SVG primitive components.
The library exports ContentLoader as the animation engine and wraps standard SVG elements
— <rect> and <circle> being the workhorses —
directly inside it. No additional CSS imports, no provider wrappers, no configuration files.
That minimal footprint is one of the reasons it remains the go-to
React skeleton loader for production codebases that care about bundle size.
For TypeScript projects, types are already bundled in the package. If you’re using Next.js,
there are zero special considerations — the component is client-side by nature,
so mark it with 'use client' in the App Router or wrap it in a dynamic import with
ssr: false in the Pages Router. Nothing exotic required.
Building Your First Skeleton Loader
The fastest way to understand the library is to build a skeleton for something concrete.
Below is a simple card loader — an avatar circle on the left, three lines of text on the right —
the kind of pattern you’d use for a social feed or comment list:
import ContentLoader from 'react-content-loader';
const CardLoader = (props) => (
<ContentLoader
speed={2}
width={400}
height={100}
viewBox="0 0 400 100"
backgroundColor="#f3f3f3"
foregroundColor="#ecebeb"
{...props}
>
{/* Avatar */}
<circle cx="48" cy="48" r="38" />
{/* Title line */}
<rect x="106" y="20" rx="4" ry="4" width="200" height="14" />
{/* Subtitle line */}
<rect x="106" y="44" rx="3" ry="3" width="140" height="10" />
{/* Body line */}
<rect x="106" y="64" rx="3" ry="3" width="250" height="10" />
</ContentLoader>
);
export default CardLoader;
The viewBox attribute is the most important prop to get right.
It defines the internal coordinate system for your SVG shapes. If your viewBox
is "0 0 400 100", then width="400" and height="100"
map directly. When the component is rendered at a different size — say, inside a responsive grid —
the SVG scales proportionally because the viewBox handles the aspect ratio.
Get the viewBox wrong and your skeleton will look like it’s been through a funhouse mirror.
The speed prop controls shimmer animation duration in seconds.
A value of 2 is the sweet spot for most interfaces — fast enough to feel alive,
slow enough not to be distracting. The backgroundColor is the base colour of the skeleton shape,
and foregroundColor is the highlight that sweeps across it.
For dark-mode UIs, swap these to something like #2d2d2d / #3d3d3d
and you’ve got dark-mode skeleton support in under a minute.
Using Built-in Presets: Facebook and Beyond
If you’ve ever visited Facebook’s feed on a slow connection, you’ve seen the canonical skeleton pattern:
a full-width placeholder with a profile circle, a name line, and two or three body text lines.
react-content-loader ships with a React Facebook loader preset that replicates this pattern
exactly, and it requires zero configuration:
import { Facebook } from 'react-content-loader';
const FeedSkeleton = () => <Facebook />;
That’s the entire component. No props required, though all the standard ones — speed,
backgroundColor, foregroundColor, width, height
— are still accepted. The library also ships with Instagram, Code,
List, and BulletList presets. These are useful as drop-in placeholders during
development and as references for what a well-structured skeleton shape layout looks like before
you build your own.
The presets are a genuine time-saver when you’re prototyping or when your actual content layout
is close enough to a standard pattern that custom shapes aren’t worth the effort.
In production, most teams end up replacing them with purpose-built loaders that match their
specific component dimensions — but the presets are an excellent starting point and a low-friction way
to introduce the React skeleton pattern to a codebase before committing to custom shapes.
Advanced Customization: Matching Real Layouts
The real power of the library shows when you build skeleton loaders that are structurally identical
to the actual components they stand in for. The workflow is straightforward:
open your real component in the browser, inspect the layout dimensions,
and mirror those dimensions as SVG shapes inside ContentLoader.
The result is a placeholder that users mentally map to real content,
which is exactly what makes the perceived-performance benefit work.
skeletonreact.com —
an interactive playground where you can draw SVG shapes visually and get the component code generated automatically.
It’s the fastest way to go from design to working skeleton without counting pixels by hand.
For a data table skeleton — one of the more demanding patterns — you’d render multiple
<rect> elements in rows, varying their widths slightly to mimic natural text length variation.
Uniform-width rows look robotic; staggered widths (say, 80%, 65%, 90%, 70%) feel more like real data.
This is a minor detail that makes a disproportionate difference to how natural the skeleton feels:
import ContentLoader from 'react-content-loader';
const TableRowLoader = ({ rows = 5, ...props }) => (
<ContentLoader
speed={2}
width="100%"
height={rows * 52}
viewBox={`0 0 800 ${rows * 52}`}
backgroundColor="#f0f0f0"
foregroundColor="#e0e0e0"
{...props}
>
{Array.from({ length: rows }).map((_, i) => {
const y = i * 52 + 14;
return (
<g key={i}>
<rect x="16" y={y} rx="3" ry="3" width="60" height="20" />
<rect x="100" y={y} rx="3" ry="3" width={160 + (i % 3) * 30} height="20" />
<rect x="320" y={y} rx="3" ry="3" width={120 + (i % 2) * 40} height="20" />
<rect x="520" y={y} rx="3" ry="3" width="80" height="20" />
<rect x="640" y={y} rx="3" ry="3" width="100" height="20" />
</g>
);
})}
</ContentLoader>
);
The rows prop makes this component reusable across different table contexts.
The viewBox scales dynamically with the row count, so the skeleton always fills
the correct vertical space. This pattern — props-driven, layout-aware, reusable —
is how mature React codebases handle react-content-loader customization
at scale rather than scattering one-off skeleton components across every feature folder.
Key Props Reference
Understanding the full props API saves you from reading source code when something behaves
unexpectedly. Here are the properties you’ll use most often, and what they actually do:
speed— Animation duration in seconds. Default:1.2. Higher = slower shimmer.viewBox— SVG coordinate system. Set this to match your shape layout dimensions.backgroundColor— Base fill colour of skeleton shapes.foregroundColor— Shimmer highlight colour. Should be slightly lighter thanbackgroundColor.backgroundOpacity/foregroundOpacity— Useful for overlaying skeletons on coloured backgrounds.uniqueKey— Critical for SSR. Ensures the gradient ID is stable between server and client renders, preventing hydration mismatches.animate— Boolean. Set tofalseto render a static placeholder (useful for reduced-motion accessibility).title— Sets the SVG<title>element for screen readers. Defaults to"Loading...".ariaLabel— ARIA label for the SVG container. Use this alongsidetitlefor full accessibility coverage.
The uniqueKey prop deserves special mention. react-content-loader generates a random
gradient ID internally — something like react-content-loader-uniquekey-abc123.
In server-side rendering environments (Next.js, Remix, Gatsby), the ID generated on the server
won’t match the one generated on the client, causing a React hydration warning.
Pass a stable, component-specific string to uniqueKey and the problem disappears entirely.
SSR, Accessibility, and Reduced Motion
Server-side rendering compatibility is one of the library’s stronger points —
provided you handle uniqueKey correctly, as covered above.
For Next.js specifically, the recommended pattern is to colocate each skeleton component
with the component it represents, give it a deterministic uniqueKey based on the component name,
and render it conditionally on a loading state from your data-fetching hook.
This keeps the skeleton as close to the real component as possible in the component tree,
which simplifies maintenance significantly.
For accessibility, the library renders an SVG with a default role="img" and a
<title>Loading...</title>. Screen readers will announce this to users.
If your loading context is more specific — “Loading user profile” or “Loading transaction history” —
pass a descriptive string to both the title and ariaLabel props.
It’s a small change with a meaningful impact for users on assistive technology.
Respecting the user’s reduced-motion preference is a separate, important concern.
The animate prop accepts a boolean, which makes it straightforward to wire up
to the prefers-reduced-motion media query via a custom hook:
import { useReducedMotion } from 'framer-motion'; // or a custom hook
import ContentLoader from 'react-content-loader';
const AccessibleLoader = (props) => {
const reduceMotion = useReducedMotion();
return (
<ContentLoader
animate={!reduceMotion}
title="Loading content"
ariaLabel="Loading content"
{...props}
>
<rect x="0" y="0" rx="4" ry="4" width="300" height="16" />
<rect x="0" y="28" rx="4" ry="4" width="220" height="16" />
</ContentLoader>
);
};
react-content-loader vs Other React Skeleton Libraries
The React loading library landscape has a few credible options, and the right choice
depends on your constraints. react-loading-skeleton is probably the most popular
alternative — it’s simpler to use because it infers skeleton dimensions from the surrounding layout
automatically. You drop a <Skeleton /> component where text or an image would be,
and it fills the space. The tradeoff is precision: for complex, asymmetric layouts, it tends to produce
skeletons that are approximately right but not exact. For simple content lists and text blocks, it’s faster.
react-placeholder takes a component-based approach where you define placeholder
components (text rows, media, shapes) and compose them like a layout. It’s more readable than raw SVG
but less flexible and harder to pixel-match against a specific design.
react-content-loader sits at the precision end of the spectrum — you control every
shape, every dimension, every corner radius. That precision is exactly what you want when your skeleton
needs to match a specific card, data table, or dashboard widget layout where the mismatch between
skeleton and content would be jarring.
Bundle size is negligible across all three options — we’re talking about 3–6 kB gzipped.
The decision comes down to workflow: if you’re scaffolding a large design system where
every component has a corresponding skeleton that needs to match exactly, react-content-loader
is the right tool. If you’re adding quick loading states to a content blog or a simple
list interface, react-loading-skeleton will get you there faster. Both are good libraries.
Neither is the wrong choice for their respective use cases.
A Real-World Pattern: Dashboard Widget Skeleton
Dashboard UIs are where react-content-loader SVG precision matters most.
A dashboard widget typically contains a header area, a large metric number, a supporting label,
and a small chart or trend indicator. Building a skeleton that mirrors this layout prevents
the jarring “content pop” that happens when a spinner disappears and the layout reflows
as real data fills inconsistently sized elements.
import ContentLoader from 'react-content-loader';
const DashboardWidgetLoader = (props) => (
<ContentLoader
speed={1.8}
width={280}
height={160}
viewBox="0 0 280 160"
backgroundColor="#ebebeb"
foregroundColor="#d6d6d6"
uniqueKey="dashboard-widget-loader"
title="Loading dashboard widget"
{...props}
>
{/* Header label */}
<rect x="16" y="16" rx="3" ry="3" width="100" height="12" />
{/* Primary metric */}
<rect x="16" y="42" rx="4" ry="4" width="140" height="32" />
{/* Supporting label */}
<rect x="16" y="88" rx="3" ry="3" width="80" height="10" />
{/* Trend bar chart — 7 bars */}
<rect x="16" y="116" rx="2" ry="2" width="20" height="28" />
<rect x="44" y="124" rx="2" ry="2" width="20" height="20" />
<rect x="72" y="110" rx="2" ry="2" width="20" height="34" />
<rect x="100" y="120" rx="2" ry="2" width="20" height="24" />
<rect x="128" y="108" rx="2" ry="2" width="20" height="36" />
<rect x="156" y="116" rx="2" ry="2" width="20" height="28" />
<rect x="184" y="104" rx="2" ry="2" width="20" height="40" />
</ContentLoader>
);
The bar chart section is a common request. Each bar is a <rect> anchored to the
bottom of its column space with varying heights — visually suggesting a real chart
without rendering actual chart data. The key insight is that the bars don’t need to match future data;
they just need to fill the visual space where the chart will live, with enough variation
to look organic rather than generated. Small adjustments to bar heights (4–8px of variation per bar)
accomplish this convincingly.
This pattern — structural fidelity without data fidelity — is the core design principle
of effective skeleton screens. You’re not showing fake data. You’re showing a believable structure
that sets accurate expectations about where things will appear. Users who understand the layout
before data loads engage with the actual content more quickly because their eye is already on the right spot.
Integrating with React Query, SWR, and Suspense
In practice, skeleton loaders live inside data-fetching components. The integration with
React Query and SWR is trivial — render the skeleton on the isLoading (React Query)
or !data (SWR) state, render the real component once data resolves.
The pattern reads clearly and requires no abstraction:
import { useQuery } from '@tanstack/react-query';
import UserCard from './UserCard';
import CardLoader from './CardLoader';
const UserCardContainer = ({ userId }) => {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <CardLoader />;
return <UserCard user={data} />;
};
With React Suspense, the skeleton can serve as the fallback in a Suspense boundary.
This approach is increasingly favoured in Next.js App Router codebases where
loading.tsx files define skeleton UIs for entire route segments.
The skeleton becomes part of the routing layer rather than scattered across individual components,
which simplifies the mental model significantly — especially in large applications with
many concurrent data-fetching operations.
One thing worth planning for: multiple simultaneous loaders. If a dashboard renders
six widgets in parallel, six separate shimmer animations are running at different phases.
You can synchronise them by passing a fixed style prop with
animationDelay offsetting each loader, or — simpler — just let them run independently.
The slight phase offset between loaders actually looks more natural than perfectly synchronised animation,
because perfectly synchronised shimmer on a multi-widget layout triggers a visual rhythm that
some users find more distracting than helpful.
Frequently Asked Questions
How do I install and set up react-content-loader in a React project?
Run npm install react-content-loader (or the yarn/pnpm equivalent), then import
ContentLoader from the package. No additional CSS or provider configuration is required.
Place SVG shape elements — <rect> and <circle> — inside the
ContentLoader wrapper and configure viewBox, backgroundColor,
foregroundColor, and speed to match your design.
For SSR environments, always pass a stable uniqueKey prop to prevent hydration mismatches.
How do I customize SVG shapes in react-content-loader?
Use standard SVG <rect> and <circle> elements as direct children
of ContentLoader. Position them with x and y, size them with
width/height or r, and round corners with rx/ry.
The shimmer animation is applied globally — you only define shape and position.
Use the interactive playground at skeletonreact.com
to build shapes visually and export the component code directly.
What is the difference between react-content-loader and other React skeleton libraries?
react-content-loader uses inline SVG, giving you precise, pixel-level control over shape,
size, and layout without any additional CSS. react-loading-skeleton uses CSS
pseudo-elements and is faster to implement but cannot exactly match complex layouts.
react-placeholder is component-composition based and more readable
but similarly constrained in layout precision. Choose react-content-loader when your skeleton
needs to mirror a specific, complex layout; use simpler alternatives for generic text and list placeholders.