How to Add a Skeleton to a React App
Step-by-step guide to adding a skeleton loading placeholder to any React app with the Drivn CLI — a copy-paste, server-safe pulse sized by Tailwind, zero deps.
Reaching for a skeleton means part of your React UI needs a placeholder while its data loads — the pulsing grey bars that stand in for a heading, an avatar, and a paragraph before the real content arrives. A spinner tells the user something is happening; a skeleton tells them roughly what is coming, which is why it reads as faster even when the wait is the same. You could hand-roll a <div className="animate-pulse bg-gray-200" /> at every call site, but that scatters a hardcoded color through your markup and gives you nothing reusable when the placeholder needs a different shape.
Drivn solves it by copy-paste: the CLI writes a skeleton.tsx file into your repository. The Skeleton is purely presentational — a single <div> whose class is bg-muted/80 rounded-md animate-skeleton merged with whatever className you pass. It holds no state and calls no browser API, so the file ships without a 'use client' directive and renders anywhere, server or client. There is no width or height prop: size, shape, and spacing are all Tailwind utilities you hand it, so one primitive covers a text line, a circular avatar, and a full media block.
This guide adds the Skeleton to any React app — install the CLI, render your first placeholder, swap it in while data loads, and restyle the pulse from the file you own. For the App Router walkthrough see the Next.js Skeleton guide, and for copy-paste layouts see the Skeleton examples.
Prerequisites
Before installing Skeleton, confirm your React project has the two things Drivn assumes: Tailwind CSS v4 installed and processing your styles, and a @/ path alias pointing at your source directory (the component ships as a .tsx file, so TypeScript is part of the stack). If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, both are already wired; for a custom setup the installation page lists the minimal config. The Skeleton is among the smallest installs in the library — its only import past React is the local cn utility for class merging, so there is no third-party package to add. The one detail worth knowing before you render it is that the placeholder has no intrinsic size: it takes its width, height, and shape entirely from the Tailwind classes you pass, so a bare <Skeleton /> collapses to nothing until you give it dimensions.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Skeleton source file. The command prompts once for your install directory (defaulting to src/components/ui/) and writes a single skeleton.tsx — no other files, because the component has no peer dependency beyond React and the cn utility. No global config file is created; the result is TypeScript source you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install. Because the file is yours after install, future Drivn releases will not overwrite your edits, and the whole component is one <div> you can read top to bottom in seconds.
1 # add the Skeleton source file 2 npx drivn add skeleton
Step 2 — Render your first skeleton
Import Skeleton and give it a size through className — the component has no width or height prop, so every dimension is a Tailwind utility. A single <Skeleton className="h-5 w-40" /> is a text line, size-12 rounded-full is a circular avatar, and h-40 w-full rounded-xl is a media block. Stack a few of them to sketch the shape of a component before its data arrives; the example below is the profile placeholder from the Skeleton docs — an avatar beside two text bars. Because size lives outside the component, the same one-line primitive becomes every shape you need with no per-shape prop to learn. Each placeholder inherits the bg-muted/80 tint, so it matches your theme in dark and light mode with no override, and the animate-skeleton class pulses them all in sync.
1 import { Skeleton } from '@/components/ui/skeleton' 2 3 export function ProfileSkeleton() { 4 return ( 5 <div className="flex items-center gap-4"> 6 <Skeleton className="size-12 rounded-full" /> 7 <div className="flex flex-col gap-2"> 8 <Skeleton className="h-5 w-40" /> 9 <Skeleton className="h-4 w-60" /> 10 </div> 11 </div> 12 ) 13 }
Step 3 — Swap the skeleton in while data loads
The point of a skeleton is to fill the gap while data is in flight. In a client component that fetches with a loading flag, branch on it: render the placeholder while isLoading is true and the real content once the data resolves. Build the fallback to mirror the shape of what replaces it — the same number of rows and roughly the same widths — so the layout does not jump when the data arrives and the skeleton reads as a preview rather than a generic bar. The example below shows the pattern with a useState loading flag, but it works the same against any data source: SWR's isLoading, React Query's isPending, or a Suspense fallback. Because the Skeleton is server-safe, you can also render it in a Server Component or a route-level loading state — the Next.js Skeleton guide covers the App Router loading.tsx pattern in full.
1 import { Skeleton } from '@/components/ui/skeleton' 2 3 export function UserCard({ isLoading, user }) { 4 if (isLoading) { 5 return ( 6 <div className="flex items-center gap-4"> 7 <Skeleton className="size-12 rounded-full" /> 8 <div className="flex flex-col gap-2"> 9 <Skeleton className="h-5 w-40" /> 10 <Skeleton className="h-4 w-60" /> 11 </div> 12 </div> 13 ) 14 } 15 return ( 16 <div className="flex items-center gap-4"> 17 <img src={user.avatar} className="size-12 rounded-full" /> 18 <div> 19 <p className="font-medium">{user.name}</p> 20 <p className="text-sm text-muted-foreground">{user.email}</p> 21 </div> 22 </div> 23 ) 24 }
Step 4 — Restyle the pulse and tint
Every class the component renders lives in one cn() call inside the file you own — the base is bg-muted/80 rounded-md animate-skeleton. Make the placeholder brighter by swapping bg-muted/80 for a lighter step; sharpen the corners with rounded-none or round them fully with rounded-full; or slow the pulse by pointing --animate-skeleton at a longer duration in your theme. The animate-skeleton utility is defined once as skeleton 2s ease-in-out infinite, and its keyframes fade the element between full opacity and 0.4 and back, so every skeleton on the page pulses together. The className you pass merges after the base through cn(), so one call site can reshape a single skeleton without touching the shared file. Because the tint reads the --muted token, adjusting it in theming re-themes every placeholder across the app at once. The verbatim source below is the whole component.
1 // verbatim from skeleton.tsx 2 export function Skeleton({ 3 className, 4 ...props 5 }: React.HTMLAttributes<HTMLDivElement>) { 6 return ( 7 <div 8 className={cn( 9 'bg-muted/80 rounded-md animate-skeleton', 10 className 11 )} 12 {...props} 13 /> 14 ) 15 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Through className. The Skeleton has no width or height prop — size, shape, and spacing are all Tailwind utilities you hand it. Pass h-4 w-40 for a text line, size-12 rounded-full for a circular avatar, or h-40 w-full rounded-xl for a media block. Because dimensions live outside the component, one <div>-based primitive covers every placeholder shape without a per-shape prop or variant to learn.
No. The Skeleton holds no state and calls no browser API — it is a styled <div> that merges a base class with any className you pass. Its source ships without a 'use client' directive, so it renders straight inside a Server Component in the Next.js App Router as well as any client-rendered React app. Nothing about it forces a client boundary, which makes it free to drop into a server-rendered page or a route-level loading state.
Branch on your loading flag: render the Skeleton layout while data is in flight and the real content once it resolves. This works against any source — a useState boolean, SWR's isLoading, React Query's isPending, or a Suspense fallback. Build the placeholder to mirror the real content's shape, matching the row count and rough widths, so the layout does not shift when the data arrives.
Yes. The animation is the animate-skeleton utility, defined once as skeleton 2s ease-in-out infinite — point --animate-skeleton at a longer duration to slow it. The tint is bg-muted/80, so recolor it by editing that class in the base or with a per-instance className, which merges after the base through cn(). Because the default reads the --muted theme token, retheming that value updates every skeleton for dark and light mode at once.
Yes. The component has no dependency on Next.js or any router — it is a self-contained styled <div> with no state, no context, and no browser API, so it works anywhere React and Tailwind reach the DOM. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add skeleton and render it the same way, sizing each placeholder with Tailwind utilities through className.

