スケルトンスクリーンは、データの取得が終わるまでの間、これから表示されるコンテンツの骨組みだけを灰色のブロックで先に描画するローディングUIです。回転するスピナーと違って「どこに何が入るのか」を先に見せられます。ただし、体感の待ち時間が必ず短くなるわけではありません。136人を対象にしたVigetの実験では、スケルトンスクリーンはスピナーにもブランク画面にも負けています。ここでは定義と呼称の整理、スピナーとの使い分けの基準、実行して確認したHTML・CSS・JavaScriptとReactの実装コード、そして競合記事が触れないアクセシビリティ対応までを扱います。
まとめ
スケルトンスクリーンは、読み込み中の領域に最終的なレイアウトと同じ位置・同じ寸法のグレーのブロックを置く待機UIです。呼び名が広まったきっかけは、Luke Wroblewskiが2013年9月17日に書いた記事でした。「スケルトンUI」「スケルトンローディング」「スケルトンローダー」はいずれも同じものを指します。
採用の判断は待ち時間で決めます。Jakob Nielsenの基準では1.0秒が「ユーザーが遅延に気づく限界」、10秒が「ユーザーの注意を引き止められる限界」です。1秒未満の領域に出すと点滅して見えるだけで、10秒を超えるなら進捗率を示せるプログレスバーに切り替えるべき場面です。
体感速度の改善だけを狙って導入するのは勧めません。Vigetの実験では、スケルトンスクリーンを見た群の体感待ち時間が2.82秒で、スピナー群の2.41秒・ブランク群の2.29秒より長く感じられました。導入する理由は、レイアウトが確定して差し替え時のずれが起きないことと、これから何が来るかを予告できることに置くのが妥当です。加えてaria-busyとprefers-reduced-motionの対応を入れないと、スクリーンリーダー利用者とモーション削減設定のユーザーには成立しません。以下、それぞれの根拠と実装を順に見ていきます。
スケルトンスクリーンの定義と呼び名
スケルトンスクリーンは、コンテンツがまだ届いていない領域に、見出し・本文行・サムネイル画像それぞれの形をした矩形を置いて画面の骨格を先に見せる手法です。ユーザーの注意を待ち時間そのものから、これから現れるコンテンツの構造へ移します。
この呼び方を広めたのはプロダクトデザイナーのLuke Wroblewskiです。2013年9月17日の記事「Mobile Design Details: Avoid The Spinner」でスピナーの欠点を指摘したうえで、「Skeleton screens are another way to focus on progress instead of wait times.」(スケルトンスクリーンは、待ち時間ではなく進行に注意を向けさせるもう1つの方法だ)と書いています。自身が発明したとは述べておらず、当時手がけていたPolarアプリでの採用例として紹介する書き方です。
スケルトンUI・スケルトンローディングとの違い
検索されるときの語は分かれていますが、指しているものは同じです。「スケルトンUI」はUIパターンの名称として、「スケルトンローディング」「スケルトンローダー」は実装やコンポーネントの名称として使われる傾向があるだけで、区別する実益はありません。英語では skeleton screen または skeleton loading と表記されます。
紛らわしいのは、同じ「スケルトン」でもプログラミングの文脈で使うスケルトンコードが別の概念だという点です。あちらは処理の中身を書く前に用意する雛形コードを指し、ローディングUIとは無関係です。
スピナー・プログレスバーとの使い分け
| 待機UI | 適する待機時間 | 進捗の提示 | 向く場面 |
|---|---|---|---|
| 表示なし | 1秒未満 | なし | 即座に返る操作 |
| スケルトンスクリーン | 1秒から10秒 | なし | レイアウトが確定している一覧・詳細 |
| スピナー | 1秒から10秒 | なし | 形が読めない領域・ボタン内 |
| プログレスバー | 10秒超 | 割合 | アップロード・インストール |
この表の時間の区切りは、Jakob Nielsenが『Usability Engineering』(1993年)で示した応答時間の3つの限界に対応しています。0.1秒は「システムが即座に反応していると感じられる限界」、1.0秒は「ユーザーの思考の流れが途切れない限界。ただし遅延には気づく」、10秒は「対話にユーザーの注意を引き止めておける限界」です。この数値自体は1968年のMillerの論文にさかのぼります。
分岐の考え方はこうです。1秒未満で返るならスケルトンもスピナーも要りません。10秒を超えるなら、あと何割かを示せない不確定なUIでは離脱を止められないので、進捗率を出せるプログレスバーが必要になります。スケルトンスクリーンが機能するのはその間、しかも表示されるレイアウトが事前に分かっている場合だけです。検索結果の件数が0件か20件か分からない領域では、骨組みを何個描くべきかが決まりません。
スケルトンスクリーンが逆効果になる条件
スケルトンスクリーンは「体感速度が上がる」と紹介されることが多いUIですが、それを検証した実験の結果は逆でした。Web制作会社のVigetが2017年10月に公開した調査では、136人に対して同じ長さの3種類のローディング演出を見せ、待ち時間の感じ方とタスク完了時間を測っています。
Vigetの実験でスケルトンが最下位になった数値
| 条件 | 人数 | すぐ読み込まれたと回答 | 体感の待ち時間 | タスク完了時間 |
|---|---|---|---|---|
| スケルトンスクリーン | 39人 | 59% | 2.82秒 | 10.54秒 |
| スピナー | 39人 | 74% | 2.41秒 | 9.49秒 |
| ブランク画面 | 58人 | 66% | 2.29秒 | 9.50秒 |
実際の演出の長さは3条件とも同一で、Vigetは「the skeleton screen performed the worst by all metrics」(スケルトンスクリーンがすべての指標で最も悪かった)と結論づけています。同意率でスピナーに15ポイント差、体感の待ち時間で0.41秒差がついています。
この結果は「スケルトンスクリーンを使うな」という意味ではありません。読むべきは採用理由のほうです。体感速度の改善を唯一の根拠にして導入すると、実装コストを払って何も得られない可能性があります。導入を正当化できるのは、差し替え時にレイアウトがずれない(後述のCLS対策)ことと、リストなのかカードなのかを先に伝えられることの2点です。この2点が要らない画面なら、実装が数行で済むスピナーのほうが妥当です。
1秒未満の領域での点滅と遅延投入
キャッシュが効いている、あるいはローカル環境で開発しているときに起きやすい失敗が、取得が数十ミリ秒で終わる領域にスケルトンを出してしまうことです。骨組みが一瞬だけ現れて消えるため、画面がちらついたようにしか見えません。Nielsenの0.1秒の限界を思い出せば、そもそも何も出さないほうが「即座に反応した」と受け取られます。
対策は2つあり、どちらか一方で足ります。取得開始から一定時間(200ミリ秒程度)を過ぎてから初めてスケルトンを描画する遅延投入か、いったん出したら最低表示時間(300ミリ秒程度)を確保してから差し替えるかです。両方を同時に入れるのは避けてください。取得が200ミリ秒から500ミリ秒程度で返る中速の環境で、遅延投入を抜けた直後に最低表示時間の待ちが上乗せされ、何もしないより遅くなります。
HTMLとCSSによるスケルトンスクリーンの実装
最終レイアウトに合わせた骨組みの寸法設計
骨組みの寸法を実際のコンテンツと合わせることが、この手法で最も重要な部分です。高さがずれていると、差し替えの瞬間に後続の要素が動いてレイアウトシフトが発生します。Core Web Vitalsで計測されるCLSはこのずれを数値化した指標で、詳しくはWebパフォーマンス指標の一覧と使い分けで扱っています。
ラッパー要素にはaria-busyを付けておきます。役割は後述しますが、マークアップの時点で入れておかないと後から差し込みにくくなります。
<div class="card" id="article-card" aria-busy="true" aria-live="polite">
<div class="skeleton skeleton--thumb"></div>
<div class="skeleton skeleton--title"></div>
<div class="skeleton skeleton--text"></div>
<div class="skeleton skeleton--text skeleton--text-short"></div>
</div>
サムネイルを差し替えるimg要素には、必ずwidthとheight属性を書きます。属性がないと画像のデコードが終わるまで高さが0のままで、スケルトンの寸法を合わせた意味がなくなります。
シマーアニメーションのCSS
下のCSSは、灰色の下地に明るい帯のグラデーションを重ね、その背景位置を横へ流すことで光沢が走る表現にしています。postcssでパースし、animationプロパティの参照名と@keyframes名が一致することまで確認したものです。
.skeleton {
background-color: #e5e7eb;
background-image: linear-gradient(90deg, #e5e7eb 0%, #f3f4f6 50%, #e5e7eb 100%);
background-size: 200% 100%;
border-radius: 4px;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
.skeleton--thumb { height: 180px; margin-bottom: 12px; }
.skeleton--title { height: 24px; margin-bottom: 12px; }
.skeleton--text { height: 16px; margin-bottom: 8px; }
.skeleton--text-short { width: 60%; }
@keyframes skeleton-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.skeleton { animation: none; background-image: none; }
}
動かすのはbackground-positionだけにしています。widthやleftを毎フレーム変える書き方だとレイアウトの再計算が走り、待ち時間を短く見せるための演出が本体の描画を遅らせる本末転倒になります。最後の@mediaブロックの意味は後半のアクセシビリティの節で説明します。
読み込み完了時の差し替え処理(JavaScript)
次のコードはjsdom上で実行し、実行前はaria-busyがtrueで骨組みの要素が4個、実行後はfalseで0個になることを確認しています。
const card = document.getElementById('article-card');
async function renderArticle(id) {
const res = await fetch(`/api/articles/${id}`);
if (!res.ok) {
card.setAttribute('aria-busy', 'false');
card.textContent = '記事を読み込めませんでした。';
return;
}
const article = await res.json();
const img = document.createElement('img');
img.src = article.thumbnail;
img.alt = '';
img.width = 320;
img.height = 180;
const title = document.createElement('h2');
title.textContent = article.title;
const body = document.createElement('p');
body.textContent = article.summary;
card.replaceChildren(img, title, body);
card.setAttribute('aria-busy', 'false');
}
差し替えにreplaceChildrenを使うと、骨組みの削除と本文の挿入が1回の呼び出しで済みます。取得したタイトルや本文はtextContentで入れてください。ここをinnerHTMLにすると、APIが返す文字列がそのままHTMLとして解釈されクロスサイトスクリプティングの経路になります。取得に失敗したときもaria-busyをfalseへ戻す点を落とさないでください。戻さないと、支援技術に対して永久に「更新中」を主張し続けることになります。
ReactとTailwind CSSでの実装
react-loading-skeleton 3.5.0での実装
Reactでは骨組み専用のコンポーネントを別に作らず、表示コンポーネント自身にローディング状態を持たせる書き方が扱いやすくなります。react-loading-skeletonの公式ドキュメントも「Don’t make dedicated skeleton screens」として、コンポーネントに組み込まれたスケルトン状態を作る方針を推奨しています。バージョンは2026年9月6日時点のnpmで3.5.0です。
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
export function ArticleCard({ article }) {
const loading = !article;
return (
<SkeletonTheme baseColor="#e5e7eb" highlightColor="#f3f4f6">
<div className="card" aria-busy={loading}>
<h2>{loading ? <Skeleton width={240} /> : article.title}</h2>
<p>{loading ? <Skeleton count={3} /> : article.summary}</p>
</div>
</SkeletonTheme>
);
}
スタイルシートの読み込みは省略できません。react-loading-skeleton/dist/skeleton.cssをimportしないと、コンポーネントは描画されるものの灰色の帯もアニメーションも出ません。
このコードをJSX変換したうえでreact-dom/serverのrenderToStringにかけると、読み込み中はspan.react-loading-skeletonが4個(width={240}の1個とcount={3}の3個)出力されました。あわせて分かったのは、ライブラリがラッパーのspanにaria-live="polite"とaria-busy="true"を自動で付けることです。手書きのCSSで組む場合はこれらを自分で付ける必要がありますが、このライブラリを使う限り骨組み部分の待機状態は伝わります。
Tailwind CSSのanimate-pulse
Tailwind CSSを使っているなら、専用のライブラリを足さずにanimate-pulseで済ませられます。このユーティリティが生成するのはpulse 2s cubic-bezier(0.4, 0, 0.6, 1) infiniteで、キーフレームは@keyframes pulse { 50% { opacity: 0.5; } }です。
<div class="w-80" aria-busy="true">
<div class="h-[180px] w-full rounded bg-gray-200 motion-safe:animate-pulse"></div>
<div class="mt-3 h-6 w-3/4 rounded bg-gray-200 motion-safe:animate-pulse"></div>
<div class="mt-2 h-4 w-full rounded bg-gray-200 motion-safe:animate-pulse"></div>
</div>
光沢が横に流れるシマーではなく、不透明度が0.5まで落ちて戻る明滅である点は先ほどのCSSと違います。motion-safe:を前置しているのは、Tailwindが用意しているmotion-safeとmotion-reduceのバリアントで、モーション削減を設定していないユーザーにだけアニメーションを適用するためです。
スケルトンスクリーンのアクセシビリティ対応
灰色のブロックはスクリーンリーダーには何も伝えません。中身のないdivだからです。視覚的には「読み込んでいる」と分かるのに、音声では無言の時間が続くという非対称がここで生まれます。実装記事の多くがこの部分を扱っていないので、コードを写しただけだと対応が抜けます。
aria-busyによる更新中の通知
MDNの定義では、aria-busyは要素が現在変更されている最中であることを示すグローバルなARIA状態です。ライブリージョンと組み合わせて使い、更新が完了するまで読み上げを遅らせる役割を持ちます。取り得る値はtrueとfalseの2つで、既定値はfalseです。読み込みが終わったらfalseへ戻します。DOMプロパティ経由なら次のように書きます。
ariaLiveElement.ariaBusy = "false";
注意すべきは、aria-busy="true"を付けても「読み込み中です」と読み上げられるわけではないことです。この属性が行うのは、更新の途中経過を細切れに通知しないよう支援技術に待たせることだけです。状態を明示的に伝えたいなら、role="status"を持つ領域に「記事を読み込み中」といったテキストを置き、完了時にそのテキストを差し替えます。実機での読み上げ順序の確かめ方はNVDA・VoiceOverでのスクリーンリーダー対応にまとめてあります。
prefers-reduced-motionによるアニメーション停止
シマーの帯は画面の幅いっぱいを一定周期で流れ続けます。前庭機能障害のあるユーザーにとって、この種の反復する動きは不快感の原因になり得ます。MDNによればprefers-reduced-motionの値はno-preferenceとreduceの2つで、reduceはユーザーが端末でモーション削減の設定を有効にしたことを示します。@media (prefers-reduced-motion)は@media (prefers-reduced-motion: reduce)と等価です。
先のCSSではこの条件下でanimationをnoneにし、グラデーションも外して単色の下地だけを残しました。アニメーションを止めても灰色のブロックは残るので、レイアウトの予告という機能は損なわれません。
色については、スケルトンのブロックは情報を持たない装飾なので、WCAGのテキストコントラスト比4.5対1を満たす必要はありません。ただし下地と背景の差が小さすぎると骨組みの存在自体が見えなくなるため、上のコード例では背景色が白の想定で#e5e7ebを使っています。どの要素に何対何が要求されるかの線引きはWebアクセシビリティのコントラスト比で整理しています。
よくある質問
スケルトンスクリーンとスケルトンUIは違うものですか?
同じものです。「スケルトンUI」はUIパターンの呼称、「スケルトンスクリーン」は画面表示としての呼称という程度の差で、実装も用途も変わりません。「スケルトンローディング」「スケルトンローダー」も同義です。
スケルトンスクリーンを入れれば体感速度は上がりますか?
上がるとは限りません。Vigetが136人に対して行った実験では、スケルトンスクリーン群の体感待ち時間が2.82秒で、スピナー群の2.41秒・ブランク画面群の2.29秒より長く感じられました。体感速度ではなく、レイアウトのずれを防ぐことと表示内容を予告することを採用理由にしてください。
スケルトン表示は何秒の待ち時間から出すべきですか?
取得に1秒以上かかる領域が目安です。Nielsenの基準で1.0秒がユーザーが遅延に気づく限界とされているためです。1秒未満で終わる領域に出すと骨組みが一瞬で消えてちらつくので、取得開始から200ミリ秒ほど待ってから描画する遅延投入を入れます。逆に10秒を超えるならプログレスバーに切り替えます。
スピナーとの違いは何ですか?
スピナーは「処理中である」ことだけを伝えるのに対し、スケルトンスクリーンは加えて「どこに何が表示されるか」を伝えます。一方でスピナーは1要素で済み、レイアウトが事前に決まっていない領域でも使えます。検索結果の件数が不定の領域や、ボタン内の小さな待機表示にはスピナーが向きます。
ライブラリを使わずに実装できますか?
できます。この記事のHTML・CSS・JavaScriptの3つのコード例がライブラリなしの実装で、CSSはlinear-gradientと@keyframesだけで構成しています。ライブラリを使う利点は、react-loading-skeletonのcountプロパティのように行数指定で骨組みを増減できることと、aria-liveなどの属性が自動で付くことです。