bones

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

OptionTypeDefaultDescription
loadingbooleanWhen 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

ParameterTypeDescription
type"text" | "block" | "container"The skeleton visual style
optionsBoneOptionsOptional configuration (only accepted for text)

BoneOptions

These options only apply to "text" bones. They are ignored for "block" and "container" types.

OptionTypeDefaultDescription
lengthnumber | MinMaxWidth in ch units. Pass a minMax(min, max) value for variable-length skeletons.
containedbooleanfalseUse 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>
  ));
}
ParameterTypeDescription
minnumberMinimum width in ch units
maxnumberMaximum 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 | undefined

repeat(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[];
}

On this page