createBones
Accepts data (or a promise) and returns skeleton helpers.
import { createBones, minMax } from "@lovo/bones";
const { bone, data, repeat, lines } = createBones(dataOrPromise, options?);createBones accepts a resolved value, a Promise, null, undefined, or the forceBones sentinel. An optional second argument controls behavior.
When only options are needed (no data to resolve), pass them as the sole argument:
const { bone, repeat } = createBones({ loading: !moveDetails });Options
| Option | Type | Default | Description |
|---|---|---|---|
loading | boolean | — | When true, forces skeleton mode regardless of the data state. Useful for client-side data fetching with hooks that expose an isLoading flag. |
const { data, isLoading } = useSWR("/api/user", fetcher);
const { bone, data: user } = createBones(data, { loading: isLoading });Return value
createBones returns four things:
bone(type, options?)
Returns HTML attributes that style an element as a skeleton while loading, or an empty object once data resolves. Spread the result onto your element.
<h2 {...bone("text", { length: 12 })}>{data?.name}</h2>Parameters
| Parameter | Type | Description |
|---|---|---|
type | "text" | "block" | "container" | The skeleton visual style |
options | BoneOptions | Optional configuration (only accepted for text) |
BoneOptions
These options only apply to "text" bones. They are ignored for "block" and "container" types.
| Option | Type | Default | Description |
|---|---|---|---|
length | number | MinMax | — | Width in ch units. Pass a minMax(min, max) value for variable-length skeletons. |
contained | boolean | false | Use inline-flex sizing instead of full-width |
minMax(min, max)
Creates a variable-length descriptor for the length option. Each bone() call within a createBones instance gets a different deterministic width between min and max (inclusive). Useful inside repeat() so skeleton lists don't all look the same.
import { createBones, minMax } from "@lovo/bones";
const { bone, data, repeat } = createBones(pokemon);
{
repeat(data?.moves, 6, (item, i) => (
<span key={item ?? i} {...bone("text", { length: minMax(4, 12), contained: true })}>
{item}
</span>
));
}| Parameter | Type | Description |
|---|---|---|
min | number | Minimum width in ch units |
max | number | Maximum width in ch units |
Bone types
"text" - Headings, paragraphs, labels. Renders a bar at the text baseline. Respects length and contained options. For multi-line text, use lines().
"block" - Images and videos. Sets a solid background and injects a transparent pixel as src.
"container" - Wrappers with complex children. Overlays a solid background and hides children with visibility: hidden.
data
The resolved value of the input. undefined while loading.
const { bone, data } = createBones(userPromise);
// data is User | undefinedrepeat(array, count, render)
Renders count placeholder items while loading, then maps over the real array once resolved. The callback receives each item (or undefined during loading) and its index.
const { bone, data, repeat } = createBones(pokemon);
{
repeat(data?.types, 2, (item, i) => (
<TypeBadge key={item ?? i} type={item} {...bone("text", { contained: true, length: 7 })} />
));
}During loading, repeat calls the callback count times with (undefined, index). Once resolved, it maps over the real array and ignores count.
A count-only signature is also available:
{
repeat(3, (_, i) => <div key={i} {...bone("text", { length: minMax(10, 20) })} />);
}lines(value, count, render)
Renders multi-line skeleton paragraphs during loading, or the real content once loaded. Your callback provides the wrapper element; lines handles the skeleton spans internally.
const { data, lines } = createBones(species);
{
lines(data?.description, 4, (item) => <p className={styles.description}>{item}</p>);
}During loading, item is an array of skeleton <span> elements (full-width lines with a shorter last line). Your callback wraps them, producing a paragraph with skeleton lines inside. Each line gets its own shimmer animation.
Once resolved, item is the real value (e.g. a string). Same callback, real text. No bone() call needed.
Type signature
function createBones(options: CreateBonesOptions): CreateBonesReturn<never>;
function createBones<T>(
data: T | Promise<T> | undefined | null,
options?: CreateBonesOptions,
): CreateBonesReturn<T>;
function minMax(min: number, max: number): MinMax;
interface MinMax {
readonly min: number;
readonly max: number;
}
interface CreateBonesOptions {
loading?: boolean;
}
interface BoneOptions {
length?: number | MinMax;
contained?: boolean;
}
interface CreateBonesReturn<T> {
bone: {
(type: "text", options?: BoneOptions): Record<string, unknown>;
(type: "block" | "container"): Record<string, unknown>;
};
data: T | null | undefined;
repeat: {
(count: number, render: (item: undefined, index: number) => ReactNode): ReactNode[];
<U>(
arr: U[] | undefined | null,
count: number,
render: (item: U | undefined, index: number) => ReactNode,
): ReactNode[];
};
lines: <V>(
value: V | null | undefined,
count: number,
render: (item: V | ReactNode) => ReactNode,
) => ReactNode[];
}