React

Use boneyard in Next.js, Vite, Remix, or any React app. Wrap your components, run the CLI, and get pixel-perfect skeleton screens.

Quick start

1. Install

bash
npm install boneyard-js

2. Wrap your components

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

function BlogPage() {
  const { data, isLoading } = useFetch('/api/post')
  return (
    <Skeleton name="blog-card" loading={isLoading}>
      <BlogCard data={data} />
    </Skeleton>
  )
}

3. Generate bones

bash
npx boneyard-js build

Auto-detects your dev server and captures all named skeletons at multiple breakpoints.

4. Import the registry

tsx
// Add once in your app entry (e.g. layout.tsx, _app.tsx, main.tsx)
import './bones/registry'

This import is required. Without it, skeletons won't render — the Skeleton component needs the registry to resolve bone data by name. Import it once at the top level of your app.

Props
PropTypeDefaultDescription
loadingboolean—Show skeleton or children
namestring—Unique name — generates name.bones.json
initialBonesResponsiveBones—Pass bones directly (overrides registry)
colorstringrgba(0,0,0,0.08)Bone color in light mode
darkColorstringrgba(255,255,255,0.06)Bone color in dark mode
animate'pulse' | 'shimmer' | 'solid'pulseAnimation style (also accepts true/false)
classNamestring—Extra CSS class on the wrapper
fallbackReactNode—Shown when loading but no bones available
fixtureReactNode—Mock content for CLI capture (dev only)
staggernumber | booleanfalseStagger delay between bones in ms (true = 80ms)
transitionnumber | booleanfalseFade out duration in ms when loading ends (true = 300ms)
boneClassstring—CSS class applied to each bone element
snapshotConfigSnapshotConfig—Controls bone extraction (see Hiding elements)

fixture prop

Use fixture to provide mock content for the CLI when real data isn't available (auth-protected pages, user-specific data, API-dependent content). Only rendered during npx boneyard-js build — never in production.

Hiding elements from the skeleton

Sometimes you don't want everything to show up in the skeleton. Maybe you have icons, decorative elements, or a live widget that should always be visible. You can tell boneyard to skip them.

Pass a snapshotConfig prop to control what gets included:

Skip specific elements by CSS class or attribute

Use excludeSelectors — any CSS selector works. The element and everything inside it gets ignored.

example
<Skeleton
  name="dashboard"
  loading={isLoading}
  initialBones={dashBones}
  snapshotConfig={{
    excludeSelectors: [
      '.icon',                     // skip all icons
      '[data-no-skeleton]',         // skip anything with this attribute
      'svg',                        // skip all SVGs
    ]
  }}
>

Skip entire HTML tags

Use excludeTags to skip every instance of a tag type. Good for nav bars and footers that shouldn't be part of the skeleton.

tsx
snapshotConfig={{
  excludeTags: ['nav', 'footer', 'aside']
}}

Mark elements in your JSX

The easiest way — add data-no-skeleton to any element you want to exclude from bone capture, then exclude it. Note: this only affects capture — the element is still hidden at runtime. Place elements outside the Skeleton wrapper to keep them visible during loading.

your-component.tsx
// No bone will be generated for this element during capture
<div data-no-skeleton>
  <LiveChart />
</div>

// Then in your Skeleton wrapper
snapshotConfig={{ excludeSelectors: ['[data-no-skeleton]'] }}

Other snapshot options

  • leafTags — Tags treated as one solid block while everything they hold is inline (default: p, h1–h6, li, td, th). A leaf tag wrapping block-level content — an li around a card, say — is walked into like a container, so a list of cards keeps its inner structure. Add span if your text renders inside span wrappers.
  • captureRoundedBorders — Set false if your cards use shadows instead of borders (default: true).
BoneSuspense — Suspense-aware skeletons

<BoneSuspense> is <Suspense> with a named <Skeleton> as the fallback. Anything that suspends — useSuspenseQuery, React.lazy, RSC streaming — shows the captured skeleton until it resolves. No loading prop to manage.

example
import { BoneSuspense } from 'boneyard-js/react'

<BoneSuspense name="user-card">
  <UserCard />  {'//'} uses useSuspenseQuery
</BoneSuspense>

With TanStack Router

TanStack Router's loader + useSuspenseQuery pattern pairs naturally with <BoneSuspense>: the loader kicks off the query, the component suspends until the cache fills, and the named skeleton renders in the meantime.

routes/users.$id.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { BoneSuspense } from 'boneyard-js/react'

export const Route = createFileRoute('/users/$id')({
  {'//'} kick the query off in the loader — don't await it, so the
  {'//'} route renders immediately and the skeleton shows while it runs
  loader: ({ context: { queryClient }, params }) => {
    queryClient.prefetchQuery(userQuery(params.id))
  },
  component: RouteComponent,
})

function RouteComponent() {
  const { id } = Route.useParams()
  return (
    <BoneSuspense name="user-card">
      <UserCard id={id} />
    </BoneSuspense>
  )
}

function UserCard({ id }: { id: string }) {
  const { data } = useSuspenseQuery(userQuery(id))
  return <Card user={data} />
}

At build time (npx boneyard-js build) the CLI's --wait window lets the query resolve so the real DOM is snapshotted. If it can't resolve at build time (auth, user-specific data), pass a fixture as the build-time fallback. The skeleton reserves the captured height while it's visible, so the layout doesn't jump when content streams in.

CLI & Vite plugin

See CLI for all build flags, watch mode, Vite plugin, and React Native scanning. See Install for the boneyard.config.json reference.