React
Use boneyard in Next.js, Vite, Remix, or any React app. Wrap your components, run the CLI, and get pixel-perfect skeleton screens.
1. Install
npm install boneyard-js2. Wrap your components
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
npx boneyard-js buildAuto-detects your dev server and captures all named skeletons at multiple breakpoints.
4. Import the registry
// 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| loading | boolean | — | Show skeleton or children |
| name | string | — | Unique name — generates name.bones.json |
| initialBones | ResponsiveBones | — | Pass bones directly (overrides registry) |
| color | string | rgba(0,0,0,0.08) | Bone color in light mode |
| darkColor | string | rgba(255,255,255,0.06) | Bone color in dark mode |
| animate | 'pulse' | 'shimmer' | 'solid' | pulse | Animation style (also accepts true/false) |
| className | string | — | Extra CSS class on the wrapper |
| fallback | ReactNode | — | Shown when loading but no bones available |
| fixture | ReactNode | — | Mock content for CLI capture (dev only) |
| stagger | number | boolean | false | Stagger delay between bones in ms (true = 80ms) |
| transition | number | boolean | false | Fade out duration in ms when loading ends (true = 300ms) |
| boneClass | string | — | CSS class applied to each bone element |
| snapshotConfig | SnapshotConfig | — | 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.
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.
<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.
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.
// 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 — anliaround a card, say — is walked into like a container, so a list of cards keeps its inner structure. Addspanif your text renders inside span wrappers.captureRoundedBorders— Setfalseif your cards use shadows instead of borders (default:true).
<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.
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.
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.
See CLI for all build flags, watch mode, Vite plugin, and React Native scanning. See Install for the boneyard.config.json reference.