Examples
Common patterns for using bones.
Suspense streaming
Pass a promise to a component wrapped in <Bones>.
import { Bones } from "@lovo/bones";
async function fetchUser(): Promise<User> {
const res = await fetch("/api/user");
return res.json();
}
export default function Page() {
return (
<Bones>
<UserCard user={fetchUser()} />
</Bones>
);
}
function UserCard({ user }: { user: Promise<User> }) {
const { bone, data, lines } = createBones(user);
return (
<div>
<img src={data?.avatar} width={64} height={64} {...bone("block")} />
<h2 {...bone("text", { length: 12 })}>{data?.name}</h2>
{lines(data?.bio, 2, (item) => (
<p>{item}</p>
))}
</div>
);
}Forced skeletons
<BonesForce> shows skeleton states without real data:
import { BonesForce } from "@lovo/bones";
function SkeletonDemo() {
return (
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
{/* Loaded state */}
<UserCard user={Promise.resolve(mockUser)} />
{/* Skeleton state */}
<BonesForce>
<UserCard />
</BonesForce>
</div>
);
}Client-side data fetching
With hooks like React Query or SWR, pass the loading option to control skeleton state:
import { createBones } from "@lovo/bones";
import useSWR from "swr";
function UserCard() {
const { data, isLoading } = useSWR("/api/user", fetcher);
const { bone, data: user, lines } = createBones(data, { loading: isLoading });
return (
<div>
<img src={user?.avatar} width={64} height={64} {...bone("block")} />
<h2 {...bone("text", { length: 12 })}>{user?.name}</h2>
{lines(user?.bio, 2, (item) => (
<p>{item}</p>
))}
</div>
);
}Skeletons show while isLoading is true, then content appears once data arrives. No Suspense boundary needed.
When you only need bone and repeat (no data to resolve), use the options-only signature:
const { bone, repeat } = createBones({ loading: !moveDetails });Multi-line text with lines()
lines generates skeleton <span> elements inside your wrapper. Full-width lines with a shorter last line, each with its own animation:
import { createBones } from "@lovo/bones";
function ArticleCard({ article }: { article: Promise<Article> }) {
const { bone, data, lines } = createBones(article);
return (
<article>
<h3 {...bone("text", { length: 20 })}>{data?.title}</h3>
{lines(data?.excerpt, 3, (item) => (
<p>{item}</p>
))}
<span {...bone("text", { length: 8, contained: true })}>{data?.readTime}</span>
</article>
);
}During loading, item is an array of skeleton spans that render inside the <p>. When data resolves, item is the real string. No bone() call needed in the callback.
List placeholders with repeat()
repeat renders a fixed number of placeholder items while data loads. The callback receives each item (or undefined during loading) and its index:
import { createBones, minMax } from "@lovo/bones";
function TagList({ pokemon }: { pokemon: Promise<Pokemon> }) {
const { bone, data, repeat } = createBones(pokemon);
return (
<div>
<h3 {...bone("text", { length: 9 })}>{data?.name}</h3>
<div style={{ display: "flex", gap: 4 }}>
{repeat(data?.types, 2, (item, i) => (
<span key={item ?? i} {...bone("text", { contained: true, length: 7 })}>
{item}
</span>
))}
</div>
</div>
);
}repeat(data?.types, 2, render) calls the callback twice with (undefined, index) while loading, giving you two skeleton pills. Once data arrives it maps over the real array and ignores the count.
minMax gives you varied widths so skeleton lists look more natural:
{
repeat(data?.moves, 10, (item, i) => (
<span key={item ?? i} {...bone("text", { length: minMax(5, 12), contained: true })}>
{item}
</span>
));
}Skeleton animations
Skeletons are static by default. Add data-bone-animate to a parent element to animate them.
Shimmer
A horizontal highlight sweep:
<div data-bone-animate="shimmer">
<Bones>
<UserCard user={fetchUser()} />
</Bones>
</div>Pulse
A gentle opacity fade:
<div data-bone-animate="pulse">
<Bones>
<UserCard user={fetchUser()} />
</Bones>
</div>Bones uses pulse automatically when prefers-reduced-motion: reduce is active, regardless of which animation you set.
Global animation
Set it on <body> to animate every skeleton in your app:
<body data-bone-animate="shimmer">Individual sections can override the global animation:
<body data-bone-animate="shimmer">
{/* These skeletons shimmer */}
<UserCard user={fetchUser()} />
{/* These skeletons pulse instead */}
<div data-bone-animate="pulse">
<Sidebar items={fetchItems()} />
</div>
</body>