Boneyard で正確なスケルトンローダーを生成する
スケルトンローダーは、コンテンツが読み込まれる前に表示されるプレースホルダーで、ユーザーに対して読み込み中であることを視覚的に示すためのものです。Boneyard はスケルトンローダーの手動の計測と更新の手間を解消するためのフレームワークです。この記事では、Boneyard を使用してスケルトンローダーを簡単に実装する方法について説明します。
スケルトンローダーは、コンテンツが読み込まれる前に表示されるプレースホルダーで、ユーザーに対して読み込み中であることを視覚的に示すためのものです。単純なローディングスピナーと比較すると、スケルトンローダーはより具体的なレイアウトを提供するためレイアウトシフトを防止したり、コンテンツの構造をユーザーが予測できるといった利点があります。
しかし、スケルトンローダーを実装するのは意外と手間がかかります。レイアウトシフトを防止するためには、実際のコンテンツと同じレイアウトを持つスケルトンローダーを作成する必要があるのですが、実際の DOM の高さを手動で計測してスケルトンローダーの高さを設定するという作業が発生します。実際の UI を変更するたびにスケルトンも更新する必要があるため、メンテナンスコストも高くなります。
Boneyard はスケルトンローダーの手動の計測と更新の手間を解消するためのフレームワークです。Boneyard を使用すると、実際のコンテンツをラップするだけで、ピクセル単位で正確なスケルトンローダーを自動的に生成できます。Boneyard は、実際のコンテンツのレイアウトを監視し、必要に応じてスケルトンローダーを更新するため、UI の変更に対しても柔軟に対応できます。
この記事では、Boneyard を使用してスケルトンローダーを簡単に実装する方法について説明します。
Boneyard の使用方法
まずは、Boneyard をプロジェクトにインストールします。
npm install boneyard-jsBoneyard を使用してスケルトンローダーを表示するためには、データ取得しているコンポーネントを Skeleton コンポーネントでラップします。<Skeleton> コンポーネントの loading プロパティが true のときにスケルトンローダーが表示され、false になると実際のコンテンツに切り替わります。
import { Skeleton } from 'boneyard-js/react'
import { activityData, type Activity } from '../data/dashboard'
// デモ用の遅延付きデータ取得フック
import { useDelayedQuery } from '../hooks/useDelayedQuery'
export function ActivityPanel() {
const query = useDelayedQuery(['activity'], activityData, 1800)
return (
<article className="card panel-card">
<SectionTitle />
<Skeleton
name="activity"
loading={query.status !== 'success'}
>
{query.data && <ActivityContent data={query.data} />}
</Skeleton>
</article>
)
}<Skeleton> コンポーネントでラップしただけでは、スケルトンローダーは表示されません。スケルトンローダーを表示するためには、Boneyard の CLI を使用して、実際のコンテンツからスケルトンローダーを生成する必要があります。
CLI を実行する前に、以下の点に注意してください。
<Skeleton>コンポーネントのnameプロパティには一意な名前を指定する必要があります。この値が、CLI で生成されるjsonファイルの名前になります。- コマンドの実行前に、プロジェクトの開発サーバーを起動しておく必要があります。
npx boneyard-js buildこのコマンドを実行すると、プロジェクトの開発サーバーの URL を特定し、Playwright を使用して実際のコンテンツをレンダリングし、スケルトンローダーを生成します。Playwright がインストールされていない場合は、自動的にインストールされます。
API からデータを取得していて実際のコンテンツのレンダリングに時間がかかる場合、正しくスケルトンローダーが生成されない可能性があります。その場合 --wait オプションで待機時間を指定するか、<Skeleton> コンポーネントの fixture プロパティでスケルトンローダーの生成に使用する固定のデータを指定できます。
<Skeleton
name="activity"
loading={query.status !== "success"}
fixture={
<ActivityContent
data={[
{
id: 1,
title: "New user registered",
timestamp: "2026-04-04T12:00:00Z",
},
{
id: 2,
title: "Server restarted",
timestamp: "2026-04-04T11:30:00Z",
},
]}
/>
}
>
{query.data && <ActivityContent data={query.data} />}
</Skeleton>スケルトンローダーはデフォルトで 375, 768, 1280px の 3 つの画面幅で生成され、src/bones ディレクトリに json ファイルとして保存されます。ブレークポイントは --breakpoints オプションで変更できます。
src/bones
├── activity.bones.json
├── metric-churn.bones.json
├── metric-mrr.bones.json
├── metric-uptime.bones.json
├── registry.js
└── team.bones.json生成された JSON ファイルはブレークポイントごとに異なるスケルトンローダーのレイアウトを定義しています。
{
"breakpoints": {
"375": {
"name": "metric-mrr",
"viewportWidth": 309,
"width": 309,
"height": 136,
"bones": [
{
"x": 0,
"y": 32,
"w": 43.5882,
"h": 34,
"r": 8
},
{
"x": 43.5882,
"y": 39,
"w": 23.7611,
"h": 33,
"r": 999
},
{
"x": 0,
"y": 88,
"w": 100,
"h": 48,
"r": 8
}
]
},
"768": {
...
},
"1280": {
...
}
}
}最後にアプリケーションのエントリーポイント(app/layout.tsx や src/App.tsx など)で src/bones/registry.js をインポートすることで、生成されたスケルトンローダーを使用できるようになります。registry.js は CLI によって自動生成される副作用インポート用のファイルで、各 .bones.json ファイルを読み込み registerBones 関数でスケルトン定義を一括登録します。
"use client"
// Auto-generated by `npx boneyard-js build` — do not edit
import { registerBones } from 'boneyard-js/react'
import _metric_mrr from './metric-mrr.bones.json'
import _activity from './activity.bones.json'
// ...
registerBones({
"metric-mrr": _metric_mrr,
"activity": _activity,
// ...
})エントリーポイントでこのファイルをインポートするだけで、すべてのスケルトンが登録されます。
import "../bones/registry";この状態でコードを実行すると、データが読み込まれる前に自動的に生成されたスケルトンローダーが表示されるようになります。確かにレイアウトシフトが最小限に抑えられていることがわかります。
実際に試してみたところ、スケルトンローダーを表示するために表示領域を確保しないため、data が undefined で子コンポーネントが何も表示されないとき、ラッパー要素の高さが 0 になり、スケルトンローダーも表示されないという問題が発生しました。<Skeleton> コンポーネントの minHeight プロパティで最小の高さを指定することで、この問題を回避できます。
Boneyard の仕組み
Boneyard ではスケルトンローダーを構成する個々の矩形ブロックを bone と呼びます。テキスト行、アバター画像、ボタンなどがそれぞれ1つの bone に対応し、位置(x, y)、サイズ(w, h)、角丸(r)の情報を持ちます。これらの bone を組み合わせることで、スケルトンローダー全体のレイアウトが形成されます。
npx boneyard-js build を実行すると、内部では以下のような処理が行われています。
まず Playwright でアプリケーションのページを開き、<Skeleton> でラップされた要素を起点に DOM ツリーを再帰的に走査します。display: none や visibility: hidden、opacity: 0 の要素はスキップされます。
走査の過程で、各要素が「リーフ(末端)」かどうかを判定します。子要素を持たない要素のほか、メディア要素(img, svg, video, canvas)、フォーム要素(input, button など)、特定のブロック要素(p, h1〜h6, li, tr)はリーフとして扱われます。
リーフ要素に対しては getBoundingClientRect() でバウンディングボックスを取得し、ルート要素からの相対位置を記録します。横方向の位置と幅はルート幅に対するパーセンテージ、縦方向はピクセル値で記録されるため、レスポンシブなレイアウトにも対応できます。border-radius も自動的に検出され、円形やピル型の要素は適切な角丸が設定されます。
リーフではないコンテナ要素であっても、背景色や背景画像、角丸ボーダーを持つ場合はコンテナ用の bone として出力されます。これにより「カード背景の上にテキストの bone が重なる」という階層的なスケルトンが表現されます。
つまり Boneyard は、ヒューリスティクスやレイアウトエンジンの再現に頼らず、ブラウザ上で実際にレンダリングされた DOM のピクセル座標をそのまま読み取ることで、正確なスケルトンを生成しています。
生成された JSON ファイルと <Skeleton> コンポーネントは name プロパティを介して紐付けられます。例えば <Skeleton name="activity"> と指定すると、activity.bones.json が対応するスケルトン定義となります。registry.js の registerBones 関数がこの名前をキーとして各 JSON ファイルをマッピングしているため、<Skeleton> コンポーネントは loading が true のときに対応する bone データを参照してスケルトンを描画できます。
実際のコードは https://github.com/0xGF/boneyard/blob/main/packages/boneyard/src/extract.ts や https://github.com/0xGF/boneyard/blob/main/packages/boneyard/src/react.tsx を参照してください。
まとめ
- Boneyard は、実際のコンテンツをラップするだけでピクセル単位で正確なスケルトンローダーを自動生成するフレームワーク
<Skeleton>コンポーネントで対象のコンポーネントをラップし、npx boneyard-js buildを実行するとスケルトンローダーの定義ファイルが生成される- データ取得に時間がかかる場合は
--waitオプションやfixtureプロパティを活用することで、正確なスケルトンローダーを生成できる - 生成されたスケルトンローダーは、エントリーポイントで
registry.jsをインポートするだけで使用できるようになる - Boneyard はブラウザ上で実際にレンダリングされた DOM のピクセル座標を読み取ることで、正確なスケルトンを生成している
