Skeleton loaders designed for React Server Components and streaming.
With React Server Components, your component renders once on the server. There's no re-render from "loading" to "loaded," so {data || <Skeleton />} doesn't work anymore. The typical workaround is writing a separate skeleton component for every piece of UI and passing it as a Suspense fallback.
Bones skips the duplication. You write your markup once and it handles both states. The skeleton and the real UI are the same component, so they can't drift apart.
createBones accepts data or a promise of data. While loading, its bone function returns HTML attributes that style elements as skeletons via CSS. Once the data resolves, bone returns an empty object and your component renders normally. There are no hooks and no context providers.
- Works in Server Components. No hooks, no context, no
'use client'. - One component handles both loading and loaded states.
- Pass a promise as a prop. Bones wires up Suspense for you.
- Skeletons are pure CSS, themed with custom properties.
- Loading elements get
aria-busy="true"automatically.
npm install @lovo/bonesImport the CSS once in your root layout or entry point:
import "@lovo/bones/css";Pass data (or a promise of data) to createBones. Spread the bone function's return value onto elements that should show skeletons while loading.
import { createBones } from "@lovo/bones";
function ProfileCard({ user }: { user: Promise<User> | User }) {
const { bone, data, lines } = createBones(user);
return (
<div>
<img src={data?.avatar} width={80} height={80} {...bone("block")} />
<h3 {...bone("text", { length: 10 })}>{data?.name}</h3>
{lines(data?.bio, 3, (item) => (
<p>{item}</p>
))}
</div>
);
}Wrap components that receive promises in <Bones>. It creates a Suspense boundary and generates the skeleton fallback for you:
import { Bones } from "@lovo/bones";
export default function Page() {
return (
<Bones>
<ProfileCard user={fetchUser()} />
</Bones>
);
}While the promise is pending, <Bones> renders the same <ProfileCard> tree with skeletons visible. Once it resolves, the real content swaps in.
| Type | Use for | Example |
|---|---|---|
"text" |
Headings, paragraphs, labels | <h2 {...bone("text")}> |
"block" |
Images, avatars, thumbnails | <img src={…} {...bone("block")} /> |
"container" |
Wrappers with complex children | <div {...bone("container")}> |
Use forceBones to see a component's skeleton state without setting up real data:
import { createBones, forceBones } from "@lovo/bones";
<ProfileCard user={forceBones} />;To force an entire subtree into skeleton mode at once, wrap it with <BonesForce>:
import { BonesForce } from "@lovo/bones";
<BonesForce>
<ProfileCard />
<PostList />
</BonesForce>;vp install # install dependencies
vp test # run tests
vp pack # build the library