Back

Automatic Skeleton Screen Generation with boneyard

Automatic Skeleton Screen Generation with boneyard

Skeleton loaders are one of those UI patterns that look simple until you actually build them. You measure DOM elements, hardcode heights, wire up conditional rendering—and then your designer changes the card layout and you do it all again. The maintenance cost quietly compounds.

boneyard-js takes a different approach: instead of writing skeleton UI by hand, it extracts layout data directly from your rendered components at dev time and generates the placeholder definitions automatically.

Key Takeaways

  • boneyard-js generates skeleton loaders automatically by capturing layout data from your real components, eliminating the need to maintain two parallel representations of the same UI.
  • DOM scanning happens at dev time through Playwright, producing static .bones.json files. No runtime DOM traversal occurs in production.
  • Capture runs at three viewport widths by default (375, 768, 1280px), with horizontal values stored as percentages for responsive behavior.
  • The fixture prop and --wait flag handle components that depend on async data unavailable during capture.
  • A Vite plugin keeps bones in sync automatically on every HMR update, removing the need for a separate CLI step.
  • Adapters exist for React, Vue, Svelte 5, Angular, Preact, and React Native, all sharing the same core format.

The Problem with Manual Skeleton Loaders

Most skeleton loader implementations are disconnected from the real UI. You build the actual component, then separately build a skeleton that approximates its shape. When the component changes, the skeleton drifts. Over time, your loading states stop matching your content, causing layout shifts and visual inconsistency.

Libraries like react-loading-skeleton reduce boilerplate, but they still require you to manually describe the structure. You’re still maintaining two representations of the same component.

How boneyard-js Generates Skeleton Screens Automatically

boneyard-js flips the workflow. You wrap your real component in a <Skeleton> tag, run a CLI command, and the tool captures the layout for you.

Here’s what that looks like in React:

import { Skeleton } from 'boneyard-js/react'

function ActivityPanel() {
  const { data, isLoading } = useFetch('/api/activity')

  return (
    <Skeleton name="activity" loading={isLoading}>
      {data && <ActivityContent data={data} />}
    </Skeleton>
  )
}

Then, with your dev server running:

npx boneyard-js build

The CLI opens a headless browser via Playwright, visits your app, finds every <Skeleton name="..."> element, and walks the DOM tree inside it. It uses getBoundingClientRect() on leaf nodes—text elements, images, buttons, form inputs—and records their position and size relative to the skeleton root. Horizontal values are stored as percentages for responsive behavior, while vertical values are stored in pixels. Border radius is detected automatically.

This process runs at three viewport widths by default (375, 768, 1280px), producing a .bones.json file per component:

{
  "breakpoints": {
    "375": {
      "bones": [
        { "x": 0, "y": 32, "w": 43.59, "h": 34, "r": 8 },
        { "x": 43.59, "y": 39, "w": 23.76, "h": 33, "r": 999 }
      ]
    }
  }
}

A registry file (registry.js or registry.ts) is also generated. Import it once at your app’s entry point and every skeleton definition becomes available globally:

import './bones/registry'

At runtime, the <Skeleton> component reads the registered bone data for its name, renders the matching layout as absolutely-positioned rectangles, and swaps them out for real content when loading becomes false.

Build-Time Capture, Not Runtime Scanning

An important distinction: the DOM scanning happens during development, not in production. The .bones.json files are static artifacts committed alongside your source code. At runtime, boneyard-js only reads those pre-generated definitions. There’s no live DOM traversal in the browser.

For React Native, the capture mechanism differs. The <Skeleton> component scans the fiber tree in dev mode using UIManager, measures native views, and sends that data to the CLI. In production builds, this scanning code is excluded entirely.

Handling Dynamic Content and the fixture Prop

If your component depends on an API that isn’t available during CLI capture, the skeleton may generate incorrectly because the content area is empty. Two options solve this:

Use --wait to delay capture after page load:

npx boneyard-js build --wait 2000

Use the fixture prop to supply static mock data specifically for capture:

<Skeleton
  name="activity"
  loading={isLoading}
  fixture={<ActivityContent data={mockData} />}
>
  {data && <ActivityContent data={data} />}
</Skeleton>

The fixture content is only rendered during CLI capture and has no effect in production.

Watch out: If your component renders nothing while data is undefined, the wrapper element collapses to zero height and the skeleton won’t display. Set a minHeight on the <Skeleton> to prevent this.

Vite Plugin for Tighter Integration

For Vite-based projects, you can skip the separate CLI terminal entirely:

// vite.config.ts
import { defineConfig } from 'vite'
import { boneyardPlugin } from 'boneyard-js/vite'

export default defineConfig({
  plugins: [boneyardPlugin()]
})

Bones are captured when the dev server starts and re-captured automatically on every HMR update. This keeps your build-time skeleton loaders in sync with your UI without any manual intervention.

Framework Support

boneyard-js ships framework-specific adapters as separate package exports:

FrameworkImport
Reactboneyard-js/react
Vueboneyard-js/vue
Svelte 5boneyard-js/svelte
Angularboneyard-js/angular
Preactboneyard-js/preact
React Nativeboneyard-js/native

The core extraction logic and .bones.json format are shared across all of them.

Is boneyard-js Worth Adopting?

boneyard-js is a relatively new tool, so expect the API to evolve. That said, the core concept is sound: generate skeleton UI from real layout data rather than maintaining it by hand.

The practical benefit is most obvious on projects where components change frequently. Instead of updating skeleton placeholders after every design iteration, you re-run one command. The .bones.json files update, and your frontend loading states stay accurate.

If you’re already spending time keeping skeleton loaders in sync with your actual UI, the automation is worth the setup cost.

Conclusion

Skeleton loaders shouldn’t drift from the components they represent, yet manual implementations almost always do. boneyard-js addresses this by treating skeletons as a generated artifact rather than a hand-written one. The capture step runs in dev, the output is a static JSON file, and the runtime cost is minimal. For teams iterating quickly on UI, that workflow saves real time and keeps loading states visually faithful to the underlying components.

FAQs

No DOM scanning occurs in production. The CLI or Vite plugin generates static .bones.json files at dev time. In production, the Skeleton component reads those definitions and renders absolutely-positioned rectangles, adding only a small runtime cost.

It captures bones at three viewport widths by default: 375, 768, and 1280 pixels. Horizontal positions and widths are stored as percentages so the skeleton scales fluidly between breakpoints, while vertical values stay in pixels for predictable spacing. The runtime selects the closest matching breakpoint based on the current viewport width.

You have two options. The --wait flag delays capture after page load to give async requests time to resolve. Alternatively, the fixture prop accepts a mock version of your component populated with static data, which renders only during CLI capture. Both approaches ensure the skeleton reflects a populated layout rather than an empty container.

Yes, and you should. The .bones.json files and registry.js are static artifacts that describe your skeleton layouts. Committing them keeps your team aligned, makes builds reproducible without running the capture step in CI, and lets code review catch unexpected layout changes alongside the component edits that caused them.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay