I've been building skeleton loaders wrong for years. Maybe you have too.
You hand-tune props. Dimensions never quite match. Design changes and skeletons drift. Layout shift happens on real data. You cry.
Then you reach for one of three solutions:
Option 1: Manual SVGs (react-content-loader)
Hand-craft every skeleton in a UI editor. Beautiful? Sure. Maintainable at 100+ components? No.
Option 2: Runtime props (react-loading-skeleton)
<Skeleton width="60%" height={32} /> and pray the dimensions match your actual component.
Option 3: Runtime heuristics (react-skeletonify)
Wrap a component, auto-generate skeletons on the fly. Clever. Also: unreliable.
All three have the same flaw: they generate skeletons at runtime.
What if we didn't?
What if we measured your components once at design-time, generated pixel-perfect skeletons, then used them as static components forever?
Enter skeletal-ui.
$ pnpm dlx skeletal-ui init
$ pnpm dlx skeletal-ui analyze
That's it. Playwright crawls your routes, measures actual DOM geometry (font-size, line-height, border-radius), generates .skeleton.tsx files.
// UserCard.tsx (your component)
export async function UserCard() {
const user = await db.users.findFirst()
return (
<div className="card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
)
}
// UserCard.skeleton.tsx (auto-generated)
import { Sk } from 'skeletal-ui'
export function UserCardSkeleton() {
return (
<div className="card">
<Sk.Avatar size={48} />
<Sk.Heading height="22px" width="55%" />
<Sk.Text lines={2} height="14px" gap="12px" />
</div>
)
}
Those dimensions? Not guesses. Playwright measured them from your real component.
Then use it like a normal component:
import { SkeletonWrapper } from 'skeletal-ui'
import { UserCard } from './UserCard'
export default function Page() {
return (
<SkeletonWrapper>
<UserCard />
</SkeletonWrapper>
)
}
Why This Changes Everything
Zero layout shift. Your skeleton's bounding box matches your content exactly. No surprises on swap.
Zero runtime overhead. It's just CSS shimmer. No JS. No DOM parsing. No magic.
40% TTI improvement. Measured on real dashboards. Layout shift elimination + runtime optimization = serious gains.
Four patterns, one API:
- React Server Components (auto-detected)
- React Query / SWR (explicit loading prop)
- React.lazy() (drop-in replacement)
- next/dynamic() (drop-in replacement)
The Comparison
| Feature | skeletal-ui | react-loading-skeleton | react-content-loader |
|---|---|---|---|
| Generation | Build-time | Runtime | Manual |
| Accuracy | Pixel-perfect (measured) | Approximate (props) | Hand-tuned (drifts) |
| Layout shift | ✅ Zero | ⚠️ Frequent | ⚠️ Frequent |
| Runtime cost | ✅ None | ⚠️ CSS + JS | ⚠️ SVG rendering |
| Maintenance | ✅ Auto-syncs | ❌ Manual updates | ❌ Redraw designs |
| Setup | ✅ One command | ⚠️ Prop tuning | ❌ UI editor work |
The Philosophy
Other skeleton libraries optimize how to generate skeletons at runtime.
skeletal-ui asks: why generate at runtime at all?
Do the hard work once. At design-time. Then ship fast.
Get Started
pnpm add skeletal-ui
pnpm dlx skeletal-ui init
pnpm dlx skeletal-ui analyze
Have you struggled with skeleton loaders? What's your biggest pain? Drop a comment — I'd love to hear your story.
Top comments (0)