17 人が閲覧(直近 30 日) React

スケルトンスクリーン(スケルトンUI)とは?スピナーとの使い分けとCSS・React・Next.jsの実装

スケルトンスクリーン(スケルトンUI)とは?スピナーとの使い分けとCSS・React・Next.jsの実装

スケルトンスクリーンは、データの取得が終わるまでの間、これから表示されるコンテンツの骨組みを灰色のブロックで先に描画するローディングUIです。スケルトンUI・スケルトンローディングも同じものを指します。回転するスピナーと違い「どこに何が入るのか」を先に見せられる一方で、体感の待ち時間が必ず短くなるわけではありません。ここでは意味と種類、Nielsen Norman Group(NN/g)の基準によるスピナー・プログレスバーとの使い分け、実行して確かめたCSS・React・MUI・Next.jsの実装、ライブラリごとに違うアクセシビリティの扱いまでを順に扱います。

まとめ

スケルトンスクリーンは、読み込み中の領域に最終的なレイアウトと同じ位置・同じ寸法のグレーのブロックを置く待機UIです。メリットは2つあります。表示される内容の形を先に伝えられることと、差し替えの瞬間にレイアウトがずれないことです。

使い分けは待ち時間で決めます。NN/gの基準では、1秒未満で終わる読み込みにはスケルトンもスピナーも要らず、スケルトンは10秒未満の待ちに使い、10秒を超えるならプログレスバーを出します。スピナーはカードや動画など1つのモジュールの読み込みに向き、画面全体の読み込みにはスケルトンが向きます。

体感速度の改善だけを理由に導入するのは勧めません。Vigetが136人に行った実験では、スケルトンを見た群の体感待ち時間が2.82秒で、スピナー群の2.41秒・ブランク画面群の2.29秒より長く感じられました。実装はCSSだけでも数十行で書け、Reactならreact-loading-skeleton、MUIならSkeleton、Next.jsのApp Routerならloading.tsxが使えます。ただしMUI 9.5.0は既定のテーマのままだとモーション削減の設定を尊重しないので、テーマでreducedMotionを指定してください。

スケルトンスクリーンの意味と種類

スケルトンスクリーンは、コンテンツがまだ届いていない領域に、見出し・本文行・サムネイル画像それぞれの形をした矩形を置いて画面の骨格を先に見せる手法です。ユーザーの注意を待ち時間そのものから、これから現れるコンテンツの構造へ移すことを狙います。

この呼び方が広まったきっかけは、プロダクトデザイナーの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とは関係がありません。

NN/gが分ける3つの種類とフレーム表示型の問題

NN/gの解説記事「Skeleton Screens 101」(2023年6月4日公開、2026年9月2日に再レビュー)は、スケルトンスクリーンを3種類に分けています。

種類 見え方 NN/gの評価
静的なコンテンツ型 ワイヤーフレーム状の灰色ブロック 最も一般的
アニメーション型 明滅(パルス)や左から右へ流れる光沢 読み込み中であることが伝わりやすい
フレーム表示型 ヘッダーなど枠だけでコンテンツの形なし 推奨しない

避けるべきなのは3つ目です。NN/gは、アプリの枠だけを出してコンテンツのプレースホルダーを含まない形を「we do not recommend」としています。画面の大半が空白のため、待ちが長引くとユーザーはページが動いていないと判断するからです。ヘッダーとフッターだけ先に描いて本文領域を白のままにする作りは、骨組みを見せているつもりでもこの型に当たります。本文領域にも見出しと行の形を置いてください。

スピナー・プログレスバーとの使い分けの基準

待機UI NN/gが示す待ち時間 向く範囲 進捗の提示
表示なし 1秒未満 即座に返る操作 なし
スケルトンスクリーン 10秒未満 画面全体・一覧・詳細 なし
スピナー 2〜10秒 カード・動画など1モジュール なし
プログレスバー 10秒超 アップロード・書き出し 割合・残り時間

NN/gは、1秒未満で読み込めるページにはスケルトンもスピナーも不要で、その場合にスケルトンを出すと煩わしく感じられるとしています。10秒を超える待ちには所要時間の明示が必要で、プログレスバーを強く推奨しています。スピナーとスケルトンの分かれ目は時間より範囲で、スピナーはダッシュボード上のカードのような単一モジュールに、スケルトンは画面全体の読み込みに向くという整理です。

1秒と10秒という区切りは、Jakob Nielsenが『Usability Engineering』(1993年)で示した応答時間の限界と一致します。1.0秒は「ユーザーの思考の流れが途切れない限界。ただし遅延には気づく」、10秒は「対話にユーザーの注意を引き止めておける限界」です。

表に当てはまらない場面が1つあります。表示されるレイアウトが事前に分からない領域です。検索結果が0件か20件か分からない一覧では骨組みを何個描くかが決まらず、件数を外すと差し替え時にずれが出ます。この場合は1件分の骨組みに留めるか、スピナーを使います。

スケルトンスクリーンのメリットと逆効果になる条件

メリット:内容の予告とレイアウトシフトの防止

1つ目のメリットは、リストなのかカードなのか、画像があるのかといった表示内容の形を、データが届く前に伝えられることです。スピナーが伝えるのは「処理中である」ことだけです。

2つ目は、骨組みを最終的なコンテンツと同じ寸法にしておけば、差し替えの瞬間に後続の要素が押し下げられないことです。このずれはCore Web VitalsのCLSとして計測されます。基準値と測り方はCore Web Vitalsとは:LCP・INP・CLSの基準値とweb-vitals・CrUX APIでの測り方で扱っています。スピナーを置いてデータ到着後に高さの違う本文を差し込む実装は、ここで点数を落としやすい作りです。

Vigetの実験で体感速度が最下位だった数値

スケルトンスクリーンは「体感速度が上がる」と紹介されることが多いUIですが、検証した実験の結果は逆でした。Web制作会社のVigetが2017年10月19日に公開した調査(Kathryn Faulkner、Katherine Olvera)では、136人に同じ長さの3種類のローディング演出を見せ、待ち時間の感じ方とタスク完了時間を測っています。

条件 人数 すぐ読み込まれたと回答 体感の待ち時間 タスク完了時間
スケルトンスクリーン 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秒の差です。

逆の傾向を示した研究もありますが、決め手にはなりません。Mejtoft・Långström・SöderströmがECCE 2018で報告した実験は14人をスケルトン群とスピナー群に分けたもので、スケルトン群のほうが知覚速度とナビゲーションの容易さの平均評価は高かったものの、論文自身が「The results of this paper cannot show any significant differences」と書くとおり、どの比較にも統計的な有意差は出ていません。

Vigetの結果は「スケルトンを使うな」という意味ではありません。導入理由を体感速度に置くと、実装の手間を払って何も得られないことがある、という意味です。採用の根拠は前の節の2つのメリットに置き、どちらも要らない画面なら数行で済むスピナーを選びます。

1秒未満の領域での点滅と遅延表示

キャッシュが効いているときやローカルの開発環境で起きやすい失敗が、取得が数十ミリ秒で終わる領域にスケルトンを出してしまうことです。骨組みが一瞬だけ現れて消えるため、画面がちらついたようにしか見えません。NN/gが1秒未満なら不要とするのはこのためです。

対策は2つあり、どちらか一方で足ります。取得開始から200ミリ秒程度を過ぎてから初めて骨組みを描く遅延表示か、いったん出したら300ミリ秒程度は表示を保ってから差し替える最低表示時間です。200ミリ秒と300ミリ秒は基準値ではなく、応答時間に合わせて調整する設定例です。遅延表示だけでは、遅延の直後に取得が終わったときの短い表示が残ります。最低表示時間は、そのちらつきを抑える代わりに待ちを延ばします。両方を併用すると、遅延を抜けた直後に最低表示時間の待ちが上乗せされるので、追加の待ちを避けたい画面では遅延表示だけにします。

HTML・CSSによるスケルトンスクリーンの実装

最終レイアウトに合わせた骨組みの寸法

骨組みは幅と高さだけでなく、余白と本文の行数も差し替え後の要素と揃えます。下の例は高さを揃えただけの最小例なので、実際のカードでは見出しと本文の余白、本文の折り返し行数まで合わせてください。ラッパー要素には後述のaria-busyを最初から付けておきます。

<div class="card" id="article-card" aria-busy="true">
  <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の属性を必ず書きます。MDNによれば、この2つの属性があればブラウザは読み込み前に縦横比を計算して表示領域を確保します。属性も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上で、正常応答・HTTP 500・通信エラー・不正なJSONの4条件を実行し、どの条件でも実行前はaria-busyがtrueで骨組みが4個、実行後はfalseで0個になることを確認したコードです。

const card = document.getElementById('article-card');

async function renderArticle(id) {
  try {
    const res = await fetch(`/api/articles/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    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);
  } catch (error) {
    card.textContent = '記事を読み込めませんでした。';
  } finally {
    card.setAttribute('aria-busy', 'false');
  }
}

replaceChildrenを使うと、骨組みの削除と本文の挿入が1回の呼び出しで済みます。取得した文字列はtextContentで入れてください。innerHTMLにするとAPIの返す文字列がHTMLとして解釈され、クロスサイトスクリプティングの経路になります。aria-busyをfinallyでfalseへ戻しているのは、HTTPエラーだけでなく通信エラーや不正なJSONで例外が出たときも解除するためです。戻さないと、支援技術に対して更新中の状態を主張し続けます。

Reactでスケルトンを表示する方法

Reactでの選択肢は、専用ライブラリ、UIフレームワーク付属のコンポーネント、CSSユーティリティ、フレームワークのルーティング機能の4つです。2026年11月5日時点のnpmの版で確かめた書き方を並べます。

react-loading-skeleton 3.5.0による行数指定とテーマ設定

表示コンポーネント自身にローディング状態を持たせる書き方が扱いやすく、react-loading-skeletonの公式ドキュメントも「Don’t make dedicated skeleton screens」として、骨組み専用の画面を別に作らない方針を勧めています。npmレジストリの最新版は、2024年9月21日公開の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は省けません。読み込まないと、コンポーネントは描画されても灰色の帯もアニメーションも出ません。react-dom/serverのrenderToStringで出力を確かめると、読み込み中はspan.react-loading-skeletonが4個(width={240}の1個とcount={3}の3個)出て、ラッパーのspanにはaria-live="polite"とaria-busy="true"が自動で付いていました。

MUI(Material UI)9.5.0のSkeleton

MUIを使っているなら付属のSkeletonで足ります。APIリファレンスのとおり、variantはtext(既定)・rectangular・rounded・circular、animationはpulse(既定)・wave・falseから選びます。

import Skeleton from '@mui/material/Skeleton';
import Typography from '@mui/material/Typography';

export function ArticleCard({ article }) {
  const loading = !article;
  return (
    <div aria-busy={loading}>
      {loading
        ? <Skeleton variant="rectangular" width={320} height={180} />
        : <img src={article.thumbnail} alt="" width={320} height={180} />}
      <Typography variant="h5">{loading ? <Skeleton width="60%" /> : article.title}</Typography>
      <Typography variant="body2">{loading ? <Skeleton animation="wave" /> : article.summary}</Typography>
    </div>
  );
}

renderToStringで確かめると、読み込み中はMuiSkeleton-rectangular・MuiSkeleton-textのクラスを持つspanが3個出力され、既定のアニメーションは2s ease-in-out 0.5s infiniteの明滅でした。Typographyの中に置いたtext型は文字の高さに合わせて寸法が決まるので、見出しと本文の行の高さを別に指定せずに済みます。react-loading-skeletonと違い、aria-busyなどの属性は付きません。ラッパーに自分で付けてください。テーマ設定やGridの使い方はMaterial UIとは:v9系の導入からGrid・テーマ設定までコード付きで解説にまとめています。

Tailwind CSS 4.3のanimate-pulse

Tailwind CSSを使っているなら、ライブラリを足さずにanimate-pulseで済みます。4.3.4のtheme.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まで落ちて戻る明滅です。motion-safe:を前に付けると、モーション削減を設定していないユーザーにだけアニメーションが適用されます。

Next.js 16のloading.tsx

Next.jsのApp Routerでは、ルートのフォルダにloading.tsxを置くだけで、そのページの読み込み中に骨組みを出せます。公式ドキュメントは「The special file loading.js helps you create meaningful Loading UI with React Suspense.」と説明しています。Next.jsはloading.tsxの中身でpage.tsxをReactのSuspense境界として包み、ページのデータ取得が終わる前に骨組みをストリーミングで先に送ります。

// app/articles/loading.tsx
export default function Loading() {
  return (
    <div aria-busy="true">
      <div className="skeleton skeleton--title" />
      <div className="skeleton skeleton--text" />
      <div className="skeleton skeleton--text" />
    </div>
  );
}

// app/articles/page.tsx
export const dynamic = 'force-dynamic';

async function getArticles() {
  await new Promise((r) => setTimeout(r, 2000));
  return [{ id: 1, title: '記事A' }, { id: 2, title: '記事B' }];
}

export default async function ArticlesPage() {
  const articles = await getArticles();
  return (
    <ul>
      {articles.map((a) => <li key={a.id}>{a.title}</li>)}
    </ul>
  );
}

Next.js 16.4.5でnext buildとnext startを実行し、/articlesのレスポンスを受信時刻つきで読むと、骨組みのaria-busyが0.25秒で届き、2秒待たせた一覧の「記事A」は2.07秒で届きました。ページ遷移時に骨組みがすぐ出るのは、loading.tsxの内容がリンク先のプリフェッチで先に取得されているためで、プリフェッチが終わっていなければ遷移は即時になりません。loading.tsxはルート単位の骨組みなので、画面の一部だけ遅い場合はその部分のコンポーネントをSuspenseで個別に包み、fallbackに骨組みを渡します。Next.jsの全体像はNext.jsとは?16系App Routerで最小アプリを動かす手順と採用判断、Suspenseの使い方はAsync Reactとは?Transition・Suspense・useOptimisticで非同期UIを組む実装で扱っています。

スケルトンスクリーンのアクセシビリティ対応

灰色のブロックはスクリーンリーダーに何も伝えません。中身のないdivだからです。画面では読み込み中と分かるのに、音声では無言の時間が続きます。骨組みの描画とは別に、読み込み状態を音声で伝える方法と、動きを減らす設定への対応が要ります。

aria-busyによる更新状態の指定と読み上げの限界

MDNの定義では、aria-busyは要素が変更されている最中であることを示すグローバルなARIA状態で、取り得る値はtrueとfalse、既定値はfalseです。読み込みが終わったらfalseへ戻します。

注意したいのは、aria-busy="true"を付けても「読み込み中です」とは読み上げられないことです。この属性が行うのは、更新の途中経過を細切れに通知しないよう支援技術を待たせることだけです。状態をはっきり伝えたいなら、role="status"を持つ領域に「記事を読み込み中」といったテキストを置き、完了時に差し替えます。実機での読み上げの確かめ方はNVDA・VoiceOverでのスクリーンリーダー対応にまとめています。

prefers-reduced-motionへの対応とライブラリの差

シマーの帯は画面の幅いっぱいを一定周期で流れ続けます。前庭機能障害のあるユーザーにとって、この種の反復する動きは不快感の原因になり得ます。端末でモーション削減を有効にするとprefers-reduced-motionがreduceになるので、その条件でアニメーションを止めます。止めても灰色のブロックは残るため、レイアウトを予告する機能は失われません。

実装 モーション削減時の既定の挙動 必要な対応
手書きCSS 動き続ける @mediaでanimation: none
react-loading-skeleton 3.5.0 光沢の帯を消す 不要
MUI 9.5.0 動き続ける テーマでreducedMotion: 'system'
Tailwind CSS 4.3 動き続ける motion-safe:を前置

差が出るのはMUIです。9.5.0のテーマはmotion.reducedMotionの既定値が'never'で、この状態のSkeletonが出力するCSSにprefers-reduced-motionの条件は含まれていませんでした。次のようにテーマで'system'を指定すると、@media (prefers-reduced-motion: reduce)の中でanimation: noneが出力されます。

import { ThemeProvider, createTheme } from '@mui/material/styles';

const theme = createTheme({ motion: { reducedMotion: 'system' } });

export function App({ children }) {
  return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}

react-loading-skeletonはskeleton.cssに@media (prefers-reduced-motion)の指定を持ち、光沢を描く疑似要素を非表示にします。

色については、テキストのコントラスト比4.5対1は文字に対する基準で、灰色のブロックには直接かかりません。一方、WCAG 2.2の達成基準1.4.11は、内容の理解に必要な図形に隣接色との3対1を求めます。白背景の#e5e7ebは計算すると1.24対1で、これには届きません。読み込み中であることはrole="status"のテキストでも伝え、灰色のブロックは補助的な表現に留めてください。要素ごとの基準はWebアクセシビリティのコントラスト比で整理しています。待機中の表示を含めて画面の体験を設計から見直す場合は、UI/UXデザインの支援で相談を受けています。

よくある質問

スケルトンスクリーンとスケルトンUIは違うものですか?

同じものです。「スケルトンUI」はUIパターンの呼称、「スケルトンスクリーン」は画面表示としての呼称という程度の差で、実装も用途も変わりません。「スケルトンローディング」「スケルトン表示」も同じ意味で使われます。

スケルトンスクリーンを入れれば体感速度は上がりますか?

上がるとは限りません。Vigetが136人に行った実験では、スケルトン群の体感待ち時間が2.82秒で、スピナー群の2.41秒・ブランク画面群の2.29秒より長く感じられました。採用の理由は、表示内容の予告とレイアウトのずれの防止に置いてください。

スケルトン表示は何秒の待ち時間から出すべきですか?

NN/gの基準では、1秒未満で終わる読み込みには出さず、10秒未満の待ちに使い、10秒を超えるならプログレスバーに切り替えます。応答時間が読めない領域では、200ミリ秒程度待ってから骨組みを描く遅延表示を入れます。200ミリ秒はNN/gの数値ではなく、応答時間に合わせて調整する設定例です。

スピナーとの違いは何ですか?

スピナーは処理中であることだけを伝え、スケルトンスクリーンは加えてどこに何が表示されるかを伝えます。NN/gはスピナーをカードや動画など1つのモジュールの読み込みに、スケルトンを画面全体の読み込みに向くとしています。レイアウトが事前に決まらない領域やボタン内の待機表示にはスピナーが向きます。

ライブラリを使わずに実装できますか?

できます。この記事のHTML・CSS・JavaScriptのコード例はライブラリなしの実装で、CSSはlinear-gradientと@keyframesだけで書いています。ライブラリを使う利点は、react-loading-skeletonのcountのように行数の指定で骨組みを増減できることと、aria-busyなどの属性やモーション削減への対応が付いてくることです。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次