Loading states are one of those details that quietly decide whether an app feels fast or janky. Spinners tell the user "wait," but they reserve no space, so the moment real content arrives the page jolts into its final shape. Skeleton screens fix that by showing a grey outline of the content while it loads — except building those outlines by hand is tedious, and they drift out of sync the instant someone tweaks a margin. Boneyard (boneyard-js) takes a different route entirely: instead of asking you to describe the skeleton, it digs the skeleton out of your real UI.
The idea is simple and a little audacious. You wrap a component in a <Skeleton>, and at build time Boneyard opens a headless browser, finds your component, measures exactly where every box lands, and saves those measurements — the "bones" — to a JSON file. At runtime, when the component is loading, Boneyard renders those captured bones as a shimmering placeholder that lines up pixel-for-pixel with the real thing. Because the outline is derived from the genuine layout, swapping the skeleton for real content produces zero layout shift, and you never hand-author a placeholder again.
It works across React, Preact, Vue, Svelte 5, Angular, and React Native, all emitting the same cross-platform bone format. That breadth, plus a build step that does the boring measurement for you, is what sets it apart from the usual crowd of skeleton libraries.
What Makes Boneyard Different
The headline feature is auto-generation, but there's more under the hood worth knowing before you reach for it.
- Captured from reality, not described. Boneyard snapshots your rendered component at multiple breakpoints (375, 768, and 1280 pixels by default), so the skeleton adapts responsively the same way your layout does.
- Zero layout shift by construction. Since the bones come from the real DOM positions, the placeholder occupies the exact footprint the content will, so nothing jumps when data arrives.
- One format, many frameworks. Whether you capture from a React app or a Svelte app, the output is the same
.bones.json, and the runtime components share an identical prop surface. - Animation built in. Choose
pulse,shimmer, orsolid, with dark-mode colors, staggered reveals, and fade-out transitions all controllable per component. - React Native without the overhead. On native, the
<Skeleton>self-measures via the fiber tree in dev mode while the CLI is running, and contributes zero overhead in production. - Suspense-friendly. A
<BoneSuspense>boundary drops in where you'd put a<Suspense>, rendering the skeleton as the fallback.
The trade-off is honest: Boneyard introduces a build step powered by a headless browser (it depends on Playwright for capture), so there are more moving parts than a purely runtime placeholder library. In exchange, you stop maintaining skeletons by hand — and you stop watching them rot every time the design changes.
Getting It Installed
Boneyard ships as a single package with framework-specific subpath imports. Install it with your package manager of choice:
npm install boneyard-js
yarn add boneyard-js
The package brings along Playwright as its one runtime dependency, used during the capture step to drive a headless browser. You only need that browser at build time — your shipped app just reads the generated bone data.
Wrapping Your First Component
The runtime API is deliberately small. You import Skeleton from the framework-specific entry point, give it a unique name, and tell it whether it's loading:
import { Skeleton } from 'boneyard-js/react'
function BlogPage() {
const { data, isLoading } = useFetch('/api/post')
return (
<Skeleton name="blog-card" loading={isLoading}>
{data && <BlogCard data={data} />}
</Skeleton>
)
}
The name is the important part: it's the key Boneyard uses to look up the captured bones for this component. Each named skeleton produces a matching blog-card.bones.json. While loading is true, Boneyard renders the bones; once it flips to false, the real <BlogCard> takes over in the identical footprint.
At this point you haven't measured anything by hand. The component renders normally in development, and the placeholder shape comes from the next step.
Generating the Bones
Wrapping a component declares "there's a skeleton here," but the actual measurements are produced by Boneyard's CLI. Run the build command and it auto-detects your dev server, visits your routes, finds every named <Skeleton>, and snapshots their layout:
npx boneyard-js build
This writes the bone files plus a registry into ./src/bones (configurable). To wire the captured data into your app, import the registry once at your entry point:
import './bones/registry'
During active development it's nicer to let Boneyard re-capture as you edit. Watch mode re-snapshots on every hot-module update, so your skeletons stay current with your layout:
npx boneyard-js build --watch
If you're on a Vite-based project — React, Preact, Vue, or Svelte — you can skip the second terminal entirely and let a plugin handle capture inside your existing dev server:
// vite.config.ts
import { defineConfig } from 'vite'
import { boneyardPlugin } from 'boneyard-js/vite'
export default defineConfig({
plugins: [boneyardPlugin()],
})
The plugin captures bones when the dev server starts and re-captures on every HMR update, so the bones evolve alongside your components without you thinking about it.
Styling the Placeholder
Because the shape is automatic, the props you tend to reach for are about look and feel rather than geometry. The <Skeleton> accepts a small set of presentation controls:
<Skeleton
name="profile-card"
loading={isLoading}
animate="shimmer"
color="rgba(0, 0, 0, 0.08)"
darkColor="rgba(255, 255, 255, 0.06)"
stagger
transition
>
{data && <ProfileCard data={data} />}
</Skeleton>
A few of these are worth calling out. animate switches between pulse, shimmer, and a static solid. The separate darkColor means you don't need a second skeleton variant for dark mode — Boneyard picks the right fill automatically. Setting stagger to true introduces an 80ms cascade so the bones reveal in sequence rather than all at once, which reads as more alive than a flat block. And transition fades the skeleton out over 300 milliseconds when loading ends, softening the handoff to real content. Both stagger and transition also accept a number if you want to tune the timing yourself.
If you'd rather set these once for the whole project, drop a boneyard.config.json at your root:
{
"breakpoints": [375, 768, 1280],
"out": "./src/bones",
"wait": 800,
"color": "#e5e5e5",
"animate": "pulse"
}
Per-component props always win over the config, so the file sets your defaults and individual skeletons override where they need to.
Skeletons for Suspense and Data Fetching
Plenty of modern React data flows lean on Suspense rather than an explicit isLoading boolean. Boneyard meets that pattern with <BoneSuspense>, a drop-in replacement for a <Suspense> boundary. The skeleton becomes the fallback at runtime, and at build time the CLI captures bones from the resolved children:
import { BoneSuspense } from 'boneyard-js/react'
function Page() {
return (
<BoneSuspense name="user-card">
<UserCard /> {/* uses useSuspenseQuery internally */}
</BoneSuspense>
)
}
There's a neat detail here: you don't need initialData or placeholderData to make capture work. Boneyard's build step waits a configurable window (the --wait flag, 800ms by default) so the query has time to resolve naturally before the snapshot is taken. If a particular query can't finish in time during capture — say it depends on a slow third-party service — you can hand the component a fixture of mock content that's used only during capture and never ships to users.
That fixture escape hatch generalizes nicely. Any component whose real data is awkward to produce at build time can supply representative stand-in content, so the captured bones reflect a realistic populated state rather than an empty shell.
Handling Authenticated and Specific Routes
Real apps aren't all public landing pages, and Boneyard has grown some practical tools for the messier cases. If part of your UI lives behind a login, you can point the capture at your already-authenticated Chrome session instead of trying to script a login:
npx boneyard-js build --cdp
The --cdp flag connects to an existing Chrome instance over its debug port and reuses that browser context, so cookies and auth state carry straight over — the bones for your dashboard get captured as the logged-in you, no credential juggling required.
When you only want to refresh one screen rather than crawl the whole app, pass the specific URL. Any URL with a non-root path triggers single-page mode, capturing just that page without following links or walking your filesystem routes:
npx boneyard-js build http://localhost:5173/settings/profile
And when a layout changes and you want to throw away the incremental cache, --force rebuilds every bone from scratch. By default Boneyard merges freshly captured bones with whatever already exists on disk, so capturing one route won't quietly drop the skeletons you generated for another.
React Native, Measured From the Device
Native is where the auto-capture approach really earns its keep, because measuring a mobile layout by hand is even more thankless than on the web. On React Native, the <Skeleton> component self-scans in dev mode whenever the CLI is running — it walks the fiber tree, measures each view through UIManager, and streams the bone data back:
import { Skeleton } from 'boneyard-js/native'
<Skeleton name="profile-card" loading={isLoading}>
<ProfileCard />
</Skeleton>
npx boneyard-js build --native --out ./bones
# Open your app on a device — bones capture automatically
Because the scanning only happens in development, there's zero measurement cost in your production bundle. Boneyard also accounts for accessibility text scaling: you generate bones at the default font size, and at runtime it scales bone positions to match whatever Dynamic Type setting the user has chosen, so the placeholder stays aligned even for someone running large text.
Where Boneyard Fits
The skeleton ecosystem already has well-loved options. react-loading-skeleton and react-content-loader are both popular, both lightweight, and both purely runtime — but they share the same premise: you compose the placeholder shape yourself, primitive by primitive, and you keep it in sync with the real UI by hand. That works, and for a single simple card it's barely any effort. The cost shows up over time, as designs shift and the hand-drawn skeleton slowly stops resembling the content it stands in for.
Boneyard makes the opposite bet. It accepts a build step and a headless-browser dependency in exchange for never hand-authoring a placeholder and never letting one drift. The skeleton is, by definition, a snapshot of the real thing. Add genuinely broad framework coverage — the same workflow and the same bone format across React, Preact, Vue, Svelte 5, Angular, and React Native — and it becomes especially appealing for teams maintaining loading states in more than one stack.
It's a young library, created in early 2026 and iterating quickly, so expect the surface to keep refining. But the core idea is unusually clean: stop describing your loading states and start capturing them. If you've ever fixed a layout shift only to watch a stale skeleton reintroduce it three sprints later, Boneyard's automatic, always-accurate bones are a genuinely refreshing way to dig your way out.