Here's the tedious part of building modern web apps: you design a beautiful UI, build it, and then you have to rebuild a fake version of that same UI as a skeleton loading state — hand-placing gray rectangles that vaguely match your layout so users see something while data loads. You measure widths, estimate positions, and hope it doesn't look jarring when the real content pops in. Then you redesign the UI and have to redo all the skeleton measurements.
Boneyard automates that entire workflow. It's a React component and CLI tool that snapshots your actual UI, extracts the positions and dimensions of visual elements, and generates a skeleton screen automatically. You build the real UI once, run a CLI command, and Boneyard creates the loading placeholder for you — positioned rectangles ("bones") that match your layout perfectly, responsive to breakpoints, with shimmer or pulse animations baked in.
I haven't deployed it yet, but the use case is too obvious: every project I've worked on has skeleton screens that drift out of sync with the real UI because updating them is manual busywork nobody prioritizes. Boneyard makes skeleton generation a build step, not a design task. That's the kind of automation that quietly saves hours across every feature.
What Boneyard Actually Does
Boneyard is two things: a React component (<Skeleton>) and a CLI tool that generates skeleton data. The workflow is:
- You build your UI component — a card, a profile header, a dashboard widget, whatever.
- You wrap it in
<Skeleton name="profile-card" loading={true}>during development. - You run the Boneyard CLI against your dev server. It loads the page, finds all
<Skeleton>components, snapshots the DOM, and extracts the positions and sizes of visible elements. - The CLI writes a JSON file of "bones" — flat arrays like
[x%, y, w%, h, borderRadius]that represent the layout. - At runtime, when
loading={true}, the<Skeleton>component renders those bones as positioned rectangles with shimmer or pulse animations. Whenloading={false}, it shows the real children.
The key insight: you don't hand-code the skeleton. You let the CLI derive it from the real UI. If you redesign the card, you re-run the CLI and the skeleton updates automatically. The skeleton is always in sync because it's generated, not maintained.
How It Works (From the Docs)
Here's a typical setup based on the README:
import { Skeleton } from 'boneyard-js'
function ProfileCard({ user, loading }) {
return (
<Skeleton
name="profile-card"
loading={loading}
fixture={<ProfileFixture />}
>
<div className="card">
<img src={user.avatar} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
</Skeleton>
)
}
The fixture prop is optional — it's an alternate version of the UI you want to snapshot, useful when real data isn't available at build time. For example, <ProfileFixture> might render placeholder text or sample data that represents the typical shape of the real component.
You run the CLI against your dev server:
boneyard-js build http://localhost:3000 --out src/bones
The CLI crawls the site, finds every <Skeleton> component, renders it (using fixture if provided, otherwise children), snapshots the DOM, and writes a JSON file like profile-card.bones.json:
{
"desktop": [[10, 20, 80, 15, 8], [10, 40, 60, 8, 4], ...],
"tablet": [[5, 15, 90, 18, 8], ...],
"mobile": [[0, 10, 100, 20, 8], ...]
}
Each array is a bone: [x%, y, width%, height, borderRadius, isContainer?]. Positions are percentages relative to the skeleton container, so they scale responsively. The CLI automatically snapshots multiple breakpoints (configurable in boneyard.config.json).
At runtime, the <Skeleton> component looks up profile-card in the auto-generated registry, renders the bones as absolutely positioned <div> elements with shimmer or pulse animations, and swaps in the real children when loading turns false.
Why This Solves a Real Problem
I've hand-coded skeleton screens on at least a dozen projects. The pattern is always the same:
- Designer shows a beautiful loading state with perfectly aligned placeholder bars.
- I measure the real component's layout, hard-code a bunch of
<div>rectangles with magic-number widths and margins. - It looks almost right on desktop, breaks on mobile, and I spend 20 minutes tweaking CSS.
- Three months later, someone redesigns the card. The skeleton doesn't get updated. Now the loading state looks nothing like the real UI, and users see a jarring layout shift when content loads.
Boneyard fixes this by making the skeleton a build artifact, not a hand-maintained component. If the real UI changes, you re-run the CLI and the skeleton updates automatically. The skeleton is always pixel-perfect because it's derived from the actual DOM, not estimated by a developer eyeballing spacing in DevTools.
It also handles responsive design automatically. Instead of writing separate skeleton CSS for mobile/tablet/desktop, you configure breakpoints in boneyard.config.json and the CLI snapshots each one. At runtime, the component picks the right bone set based on viewport width. That's a lot of manual work eliminated.
The Workflow I'm Planning to Test
Here's how I'd integrate Boneyard into a real project:
- Wrap loading components: For every component that loads async data (user profiles, product cards, dashboard widgets), wrap the real content in
<Skeleton name="..." loading={isLoading}>. - Create fixtures: For components that need real data to render (e.g., a chart with live data), create a fixture version with sample data that represents the typical layout.
- Run CLI in CI: Add
boneyard-js buildto the build pipeline. Every time the UI changes and gets deployed, the skeletons regenerate automatically. - Configure snapshots: Use
snapshotConfig={{ leafTags: ["section"] }}to control granularity. For example, treat a whole card as a single bone instead of snapshotting every text line and icon inside it. - Review in Storybook: Set up Storybook stories with
loading={true}to visually QA the skeletons and make sure they match the real UI.
The goal: skeleton screens become a zero-maintenance side effect of building the real UI. If it works as advertised, I'd never hand-code a loading placeholder again.
Configuration and Customization
Boneyard uses a boneyard.config.json file for global defaults:
{
"breakpoints": [375, 768, 1280],
"color": "#e0e0e0",
"darkColor": "#333333",
"animate": "shimmer",
"speed": 1.5,
"shimmerColor": "rgba(255, 255, 255, 0.3)"
}
You can override these per-component with props:
<Skeleton
name="profile-card"
loading={loading}
animate="pulse"
color="#f5f5f5"
speed={2}
/>
The CLI bakes the config into the generated registry.js so there's no runtime config lookup — the defaults are already inlined. That's a nice performance touch: no extra network requests or parsing at runtime.
Dark mode is detected via a .dark class on <html> or an ancestor element. It doesn't use prefers-color-scheme — the app developer has explicit control. When dark mode is active, the component swaps color for darkColor. That means the skeletons automatically adapt to your theme without extra code.
Potential Rough Edges (Based on the README)
I haven't tested this yet, but based on the docs and code structure, here are the likely friction points:
- Build-time dependency: The CLI needs a running dev server to snapshot. That means you can't generate skeletons in a static build environment unless you spin up the server first. For CI/CD, you'd need to run
npm run dev, wait for it to start, run the CLI, then kill the server. That's extra complexity in the pipeline. - Snapshot accuracy: If the fixture doesn't match the real UI's typical layout (e.g., you use short placeholder text but real text is usually longer), the skeleton will be wrong. You need to carefully design fixtures that represent the average case, not the edge case.
- Dynamic layouts: If your UI layout changes significantly based on data (e.g., a card with an optional image that changes the whole layout), a single snapshot won't cover both cases. You'd need multiple named skeletons or fallback logic.
- leafTags tuning: The extractor recursively walks the DOM and creates bones for every visible element. For complex components, that can generate dozens of tiny bones (one for each text node, icon, etc.). You need to configure
leafTagsto tell it "treat this whole section as one bone" to avoid over-granularity. That requires some trial and error. - Registry bloat: If you have hundreds of components, the auto-generated
registry.jscould get large. The README mentions bones are hashed and only changed skeletons are rewritten, but the registry itself is a single file that imports all bones. That could affect bundle size if you're not tree-shaking carefully.
How It Compares to Manual Skeleton Coding
The traditional approach to skeleton screens:
- Hand-code a separate skeleton component with hard-coded rectangles and spacing.
- Maintain it manually every time the real UI changes.
- Write separate responsive CSS for mobile/tablet/desktop.
- Hope it stays in sync with the real design (it usually doesn't).
The Boneyard approach:
- Wrap the real component in
<Skeleton>. - Run a CLI command to snapshot the layout.
- The skeleton is auto-generated and responsive.
- Re-run the CLI when the UI changes to regenerate.
The tradeoff: you add a build step and a dev server dependency, but you eliminate all the manual measurement, CSS tweaking, and maintenance. For projects with dozens of loading states, that's a huge win. For projects with one or two simple skeletons, it might be overkill — hand-coding two rectangles is faster than setting up a build pipeline.
Use Cases I'm Planning to Test
Here's where I'd try Boneyard first:
- Dashboard widgets: A dashboard with 10+ widgets that each load async data. Each widget has a different layout. Instead of hand-coding 10 skeletons, wrap them in
<Skeleton>, snapshot once, done. - E-commerce product cards: Product grids where each card has an image, title, price, and rating. The skeleton needs to match the card layout exactly to avoid layout shift. Boneyard auto-generates it and keeps it in sync as the card design evolves.
- User profiles: Profile pages with avatar, bio, stats, and activity feed. The layout is complex and responsive. Instead of writing separate mobile/desktop skeletons, let Boneyard snapshot both breakpoints.
- Data tables: Tables that load server-side data. The skeleton needs to show placeholder rows with the right column widths. Boneyard can snapshot a fixture table with sample rows and generate the skeleton automatically.
The common thread: components with non-trivial layouts where hand-coding a pixel-perfect skeleton is tedious and error-prone, and where the real UI might change over time.
Pairing It With the Rest of the Frontend Stack
Boneyard fits into a React/Next.js workflow alongside other tools:
- Boneyard: Generates skeleton loading states from real UI.
- ESLint: Lints the generated registry and component code to catch issues.
- ReviewDog: Runs linters in CI and flags skeleton config issues inline.
- Storybook: Visualize skeletons in isolation with
loading={true}to QA them before shipping. - React Suspense: Trigger
<Skeleton loading={true}>automatically when Suspense boundaries are loading.
The ideal workflow: build the real UI, wrap it in <Skeleton>, run the CLI in CI to regenerate bones on every deploy, visually QA in Storybook, ship. The skeleton is always in sync because it's generated as part of the build, not maintained separately.
Why This Bookmark Is High-Priority
Two weeks ago I redesigned a user profile card. The card layout changed — avatar moved from left to top, bio got longer, stats rearranged. I shipped the new design. Three days later someone noticed the skeleton loading state (which I forgot to update) looked completely wrong — the placeholder rectangles were in the old positions. Fixing it took 15 minutes of CSS tweaking and visual comparison to make sure the skeleton matched the new layout.
That's 15 minutes I shouldn't have spent. If I'd used Boneyard, the skeleton would have regenerated automatically when I rebuilt the card. No manual update, no forgetting, no visual drift. That failure mode — skeletons drifting out of sync with real UI — happens on every project I've worked on. Boneyard eliminates it by making skeleton generation a build artifact, not a manual task.
I'm bookmarking it to test on the next project with significant loading states. If it works reliably and the CI integration is smooth, it becomes part of the default frontend stack. The ROI is clear: eliminate a whole category of manual, error-prone busywork that nobody enjoys but everybody has to do.
The Test Plan
Here's how I'll validate whether Boneyard is worth adopting:
- Install: Add
boneyard-jsto a Next.js project, set upboneyard.config.jsonwith breakpoints and animation preferences. - Wrap a simple component: Start with a single card component, wrap it in
<Skeleton>, create a fixture if needed. - Run CLI locally: Run
boneyard-js build http://localhost:3000 --out src/bonesand inspect the generated bones JSON. Verify the positions and sizes look correct. - Test loading state: Toggle
loading={true/false}and verify the skeleton renders correctly, animations work, and the transition to real content is smooth. - Test responsive: Resize the browser and verify the skeleton adapts to mobile/tablet/desktop breakpoints without layout shift.
- Redesign the component: Change the card layout, re-run the CLI, verify the skeleton updates to match. This tests the core promise: auto-sync on UI changes.
- Add to CI: Integrate the CLI into the build pipeline, ensure it runs on every deploy without manual intervention.
- Expand to complex components: Try it on a dashboard widget, a data table, or a profile page with multiple sections. See if
leafTagstuning is intuitive or frustrating.
If steps 1-6 work smoothly and the generated skeletons are high-quality without extensive tuning, Boneyard becomes a default tool. If the CLI is flaky, the snapshots are inaccurate, or the tuning is too manual, I'll fall back to hand-coding skeletons for complex cases and only use Boneyard for simple cards and lists.