Drivn vs shadcn/ui — Skeleton Compared
Compare Drivn Skeleton vs shadcn/ui — a custom opacity keyframe on bg-muted/80 versus Tailwind's animate-pulse on bg-accent. Both copy-paste, both server-safe.
Drivn's Skeleton and shadcn/ui's Skeleton are the closest two files in either library — single-element divs that exist only to flash a placeholder while real content loads. Both are server-safe, both accept a className, both forward HTML attributes, and both rely on Tailwind for the animation. The split is in two specific choices: the animation keyframe and the surface colour token.
shadcn/ui's Skeleton applies animate-pulse rounded-md bg-accent — animate-pulse is the Tailwind built-in that uses a cubic-bezier easing to fade opacity to 0.5 and back over two seconds. The bg-accent token resolves to the theme's subtle hover surface. Drivn's Skeleton applies bg-muted/80 rounded-md animate-skeleton, where animate-skeleton is a custom keyframe declared in globals.css: 0%, 100% { opacity: 1 } and 50% { opacity: 0.4 } over two seconds of ease-in-out. The surface is bg-muted at eighty percent opacity, so the placeholder picks up the same muted token used elsewhere in the design system but stays slightly translucent against the background.
The practical difference is taste — slower fade, softer floor, theme-coordinated muted token — wrapped around the same one-line div. This page walks the keyframe, the colour token, the className escape hatch, and the server-component story so you can pick the flavour that fits.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying element | Single div with HTMLAttributes spread | Single div with HTMLAttributes spread |
| Runtime UI dependencies | cn() utility only | cn() utility only |
| Animation | animate-skeleton (custom keyframe, ease-in-out, opacity 1 → 0.4) | animate-pulse (Tailwind built-in, cubic-bezier, opacity 1 → 0.5) |
| Surface token | bg-muted/80 | bg-accent |
| Shape | rounded-md (override via className) | rounded-md (override via className) |
| Client component | No — server-safe | No — server-safe |
| Props | className + HTMLAttributes<HTMLDivElement> | className + HTMLAttributes<HTMLDivElement> |
| License | MIT | MIT |
| Copy-paste install |
A custom keyframe instead of animate-pulse
shadcn/ui reaches for Tailwind's built-in animate-pulse. The class compiles to a pulse keyframe that fades opacity between 1 and 0.5 with a cubic-bezier easing curve over two seconds, infinite. It is fine, but the floor is high — 0.5 opacity is still very visible — and the cubic-bezier easing makes the dim phase feel snappier than a slow shimmer.
Drivn ships its own animate-skeleton keyframe declared in src/styles/globals.css and exposed as a Tailwind animation utility. The keyframe sets 0%, 100% { opacity: 1 } and 50% { opacity: 0.4 }, runs for two seconds with ease-in-out, and loops forever. Lower floor (0.4 vs 0.5), softer easing, longer perceived pause at the peaks. The component file uses that utility on a single div, so swapping the timing means editing one declaration in globals.css and every Skeleton in the project follows.
1 /* src/styles/globals.css — verbatim */ 2 --animate-skeleton: skeleton 2s ease-in-out infinite; 3 4 @keyframes skeleton { 5 0%, 100% { opacity: 1; } 6 50% { opacity: 0.4; } 7 }
bg-muted/80 instead of bg-accent
The colour choice is the second visible split. shadcn's current Skeleton uses bg-accent, which resolves to the theme's subtle hover surface — a near-neutral that sits just above the page background. The block stays solid and reads as a flat shape.
Drivn uses bg-muted at eighty percent opacity. bg-muted is the same token the text-muted-foreground colour pairs against, so the placeholder visually belongs to the same secondary surface used elsewhere in the design system. The trailing /80 keeps the block slightly translucent — when stacked over a coloured panel or a card with its own background, the Skeleton tints rather than blocks. Pair it with the Card for product cards or the Avatar for a circular profile placeholder and the muted-with-opacity choice lets the surrounding container show through.
1 // Drivn — verbatim from the registry 2 return ( 3 <div 4 className={cn( 5 'bg-muted/80 rounded-md animate-skeleton', 6 className 7 )} 8 {...props} 9 /> 10 )
Sizing and shape live entirely in className
Both libraries put the sizing decision on the caller. The base classes — bg-muted/80 rounded-md animate-skeleton in Drivn, animate-pulse rounded-md bg-accent in shadcn — set the surface and the animation; the width, height, and (optional) round-pill shape come from the className you pass. There is no size prop, no variant union, no width/height attributes.
The Skeleton docs preview leans on this directly: a 12-unit circle (size-12 rounded-full) for the avatar slot, a 5-unit-tall by 40-unit-wide rectangle for a heading line, and a 4-unit-tall by 60-unit-wide rectangle for the body line beneath it. The rounded-md in the base class only matters when you do not pass your own rounding utility — rounded-full, rounded-none, or any rounded-{size} later in the className wins thanks to cn(). Treat the component as a styled <div> whose only job is to be the right shape for the content it is standing in for.
1 import { Skeleton } from "@/components/ui/skeleton" 2 3 export default function UserCardSkeleton() { 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 }
Server-safe, no hydration cost
Neither component is marked 'use client'. Both are plain functions that spread HTMLAttributes<HTMLDivElement> onto a single div, which means both can render directly inside server components, layouts, and statically rendered pages. The animation is CSS — the browser drives it, not React — so there is no JavaScript cost beyond the bytes of the component file itself.
That matters for loading states inside route segments that you want to keep server-rendered. A product grid that streams in with Suspense fallbacks, a profile page with an avatar placeholder above the fold, a list page with row-shaped skeletons — none of these need a client boundary just because they show a Skeleton during data fetch. Pair the component with a <Suspense fallback={<UserCardSkeleton />}> boundary and the fallback renders on the server, the animation plays via CSS, and the real content swaps in when the data resolves. Use the Drivn CLI to add the file to your project in one command.
1 // app/profile/page.tsx — server component, no 'use client' 2 import { Suspense } from "react" 3 import { Skeleton } from "@/components/ui/skeleton" 4 import { ProfileCard } from "./profile-card" 5 6 export default function ProfilePage() { 7 return ( 8 <Suspense 9 fallback={ 10 <div className="flex items-center gap-4"> 11 <Skeleton className="size-12 rounded-full" /> 12 <Skeleton className="h-5 w-40" /> 13 </div> 14 } 15 > 16 <ProfileCard /> 17 </Suspense> 18 ) 19 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
The default animate-pulse keyframe fades to 0.5 opacity with a cubic-bezier curve. Drivn's animate-skeleton fades to 0.4 with ease-in-out — slightly lower floor, slightly softer easing, slightly longer perceived pause at full opacity. The keyframe is declared in src/styles/globals.css, so tweaking the timing or the opacity floor is a one-line change that updates every Skeleton at once.
Yes. Both the surface (bg-muted/80) and the shape (rounded-md) are in the base className, and cn() merges with later-class precedence. Pass className="bg-primary/10" for a brand-tinted placeholder, className="rounded-full" for a circular avatar slot, or className="rounded-none" for a sharp-edged bar. Width and height live entirely in className via the usual Tailwind utilities (h-4, w-60, size-12).
No. The component has no 'use client' directive, no hooks, no refs, and no event handlers. It spreads HTMLAttributes<HTMLDivElement> onto a single div with the animation class. You can render it inside server components, layouts, statically generated pages, and Suspense fallbacks without crossing a client boundary or adding to the JavaScript bundle for that route.
A spinner indicates that something is loading without showing where. A Skeleton indicates loading and reserves the exact shape of the content that is about to appear — same height, same width, same rounded corners. The layout does not shift when the real content arrives, which avoids the cumulative layout shift cost that a spinner-to-content swap can introduce on a Core Web Vitals score.

