テクニック

スケルトンスクリーンのCSS実装方法|読み込み中の見せ方とコード

この記事の対象: ローディング表示を実装するWeb制作者・フロントエンドエンジニア
読了時間: 約9分

スケルトンスクリーンは「待ち時間を短く感じさせる」ための装飾ではなく、読み込み完了後に出てくる中身の形を、先に場所取りしておく仕組みです。これを入れると、データが届いた瞬間に文字や画像がガクンと飛び出してレイアウトが動く現象(CLS)が消えます。逆に言えば、寸法を実物と合わせていないスケルトンは、問題を解決しないどころかチラつきを一段増やすだけの飾りになります。

この記事では、スピナーとの使い分けの線引き、コピーしてそのまま動くCSS、光が流れる演出の付け方、そして止めどころ(prefers-reduced-motion・コントラスト・描画コスト)までをまとめます。案件の種類ごとに「使う/使わない」も最後に言い切ります。


スケルトンスクリーンとスピナーの使い分け

スケルトンスクリーンとスピナーの使い分け

どちらも「待たせている」ことは変わらない

スピナーもスケルトンも、データが届く速度を1ミリ秒も速くしません。変わるのは、待っているあいだに画面が何を伝えているかだけです。

スピナーが伝えるのは「処理中」という事実ひとつ。スケルトンが伝えるのは「この位置にタイトル、その下に3行の本文、左に正方形の画像が出ます」という構造の予告です。予告があると、人は届く前に目線のルートを組み立て始めます。到着した瞬間に読み始められるのはそのためです。

判断は「出てくるものの形が決まっているか」で分ける

迷ったときは、完了後のDOMの形が事前にわかるかどうかで切ります。

スケルトンとスピナーの使い分け

スケルトンが向く

  • 記事一覧・商品カードなど形が固定された繰り返し
  • プロフィール・ダッシュボードの初回読み込み
  • 無限スクロールの追加読み込み
  • 0.3〜3秒くらいで返ってくる見込みのもの

スピナーが向く

  • 結果の形が予測できない検索・絞り込み
  • ボタンを押した直後の送信処理
  • PDF生成・決済のように結果が1点だけのもの
  • 0.3秒未満で終わる、または10秒以上かかるもの

10秒を超える処理にスケルトンを置くと、灰色の板が延々と点滅し続ける画面になります。長時間かかるものは、スピナーではなく進捗率か処理内容のテキストを出すほうが正解です。「画像を変換しています(3/12)」のほうが、何秒待てばいいかの手がかりになります。

0.3秒未満で終わるものには、どちらも置きません。表示して即座に消える灰色の板は、ただのチラつきです。後述する遅延表示で回避します。

同時に出さない

スケルトンの上に重ねてスピナーも回す実装を時々見ますが、これは情報として冗長なうえ、アニメーションが2系統走って描画コストも倍になります。どちらか一方にします。


CSSだけで作るスケルトンの基本形

CSSだけで作るスケルトンの基本形

必要なのは背景色・角丸・寸法の3つだけ

まずアニメーションなしの、いちばん素の形です。これだけでCLSは防げます。

<div class="skeleton-card" aria-hidden="true">
  <div class="sk sk-thumb"></div>
  <div class="sk-body">
    <div class="sk sk-title"></div>
    <div class="sk sk-text"></div>
    <div class="sk sk-text"></div>
    <div class="sk sk-text sk-text--short"></div>
  </div>
</div>
.skeleton-card {
  display: flex;
  gap: 16px;
  padding: 16px;
  max-width: 640px;
  background: #fff;
  border-radius: 12px;
}

.sk {
  background-color: #e9ecef;
  border-radius: 6px;
}

.sk-thumb {
  flex: 0 0 120px;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
}

.sk-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 4px;
}

.sk-title {
  height: 20px;
  width: 65%;
}

.sk-text {
  height: 13px;
  width: 100%;
}

.sk-text--short {
  width: 42%;
}

ポイントは3つあります。

高さは height で直接指定する。line-height に任せると、中身が空の要素は潰れます。文字のプレースホルダは、実際の文字サイズではなく「文字の黒い部分が占める高さ」に合わせると自然に見えます。本文16pxなら12〜14px、見出し24pxなら18〜20pxが目安です。文字高そのままにすると、灰色の板が太すぎて圧迫感が出ます。

最後の行だけ短くする。全部の行を100%幅にすると、不自然に整った長方形の集合になり、文章には見えません。最終行を40〜60%にするだけで、段落らしさが出ます。

画像枠は aspect-ratio で確保する。width と height を両方書く方法でも成立しますが、レスポンシブで幅が変わる場合は aspect-ratio のほうが実物と一致させやすくなります。実際の <img> 側にも同じ aspect-ratio を指定しておくと、差し替わった瞬間のズレがゼロになります。

aspect-ratio は主要ブラウザで利用できますが、古い端末を含む案件では開発者ツールで実機確認するか、padding-top でのアスペクト比確保に切り替えてください。

寸法を実物と合わせないと意味がない

スケルトンの寸法が実物と違うと、差し替わる瞬間に要素が動きます。これはスケルトンを入れていない状態より悪い結果です。何も出していなければ1回しか動きませんが、寸法のズレたスケルトンがあると「空→スケルトン→実物」で2回動くからです。

確実に合わせるには、同じCSSクラスで寸法を共有します。

/* 実物とスケルトンが同じ箱を共有する */
.card-thumb,
.sk-thumb {
  flex: 0 0 120px;
  aspect-ratio: 1 / 1;
  border-radius: 8px;
}

.card-title,
.sk-title {
  font-size: 18px;
  line-height: 1.6;
  min-height: calc(18px * 1.6);
}

タイトルが2行に伸びる可能性があるなら、スケルトンも2行分の高さにしておくか、実物側に min-height で下限を敷いて揺れ幅を潰します。文字サイズを clamp() で可変にしている場合は、スケルトン側にも同じ clamp() 式を当てないと画面幅によってズレます。


光が流れる演出(シマー)の付け方

グラデーションを動かす方法が最も軽い

左から右へ光が走る、いわゆるシマー効果です。実装方法はいくつかありますが、background-position を動かす方法が負荷と記述量のバランスで最も扱いやすいです。

.sk {
  position: relative;
  background-color: #e9ecef;
  background-image: linear-gradient(
    100deg,
    rgba(255, 255, 255, 0) 20%,
    rgba(255, 255, 255, 0.65) 50%,
    rgba(255, 255, 255, 0) 80%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  background-position: -120% 0;
  border-radius: 6px;
  animation: sk-shimmer 1.4s ease-in-out infinite;
}

@keyframes sk-shimmer {
  to {
    background-position: 220% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sk {
    animation: none;
    background-image: none;
  }
}

100deg とわずかに傾けているのは、真横(90deg)だと機械的に見えるからです。1〜3度ずらすだけで、紙の上を光が滑るような角度になります。

速度は 1.2〜1.8秒の範囲に収めます。これより速いと落ち着きがなく、遅いと止まって見えます。infinite で回しますが、データが届いたらスケルトンのDOMごと消すので、無限ループが残り続けることはありません。

白を重ねる方式が使えない場面

上のコードは「灰色の上に半透明の白を流す」仕組みなので、背景が暗い画面では光が見えません。ダークテーマでは、ベースを暗く、流す色を明るいグレーにします。

@media (prefers-color-scheme: dark) {
  .sk {
    background-color: #2a2d31;
    background-image: linear-gradient(
      100deg,
      rgba(255, 255, 255, 0) 20%,
      rgba(255, 255, 255, 0.08) 50%,
      rgba(255, 255, 255, 0) 80%
    );
  }
}

ダークでは不透明度を 0.65 ではなく 0.06〜0.10 まで落とします。ライトと同じ強さで白を流すと、暗い画面の中で光だけが強烈に光って目に刺さります。

重い書き方を避ける

シマーの実装で避けたほうがいい方法が3つあります。

書き方 問題
filter: blur() で光をぼかす 毎フレーム再描画が走る。カードが10枚並ぶと一気に重くなる
box-shadow をアニメーション 影の再計算がフレームごとに発生する
画像(GIF/PNG)を背景に敷く 転送量が増える。読み込みを速く見せるための要素が読み込みを増やすのは本末転倒
width や left をアニメーション レイアウト再計算が走る。transform か background-position に逃がす

background-position のアニメーションも厳密には合成だけで済むわけではありませんが、filter 系に比べれば桁違いに軽く、要素数が数十枚のリストでも実用に耐えます。

心配なら、Chromeの開発者ツール→Performanceでスケルトン表示中を数秒記録し、Renderingの帯がどれだけ埋まるかを見ます。数値で判断がつきます。

もっと軽く見せたい場合は、シマーをやめて不透明度だけを呼吸させる手もあります。

@keyframes sk-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.sk--pulse {
  background-color: #e9ecef;
  animation: sk-pulse 1.6s ease-in-out infinite;
}

opacity は合成だけで処理されるため、シマーよりさらに軽くなります。カードが30枚以上並ぶ一覧なら、こちらを選びます。

背景のグラデーション表現そのものを詰めたい場合は、この記事も参考になります。


アクセシビリティで必ず押さえる3点

アクセシビリティで必ず押さえる3点

1. prefers-reduced-motion で止める

繰り返し動き続けるアニメーションは、前庭障害や光過敏のある人にとって不快感や症状の引き金になります。スケルトンは画面に数十個が同時に点滅するため、影響が大きい部類です。

止め方は2通りあり、使い分けます。

@media (prefers-reduced-motion: reduce) {
  /* アニメーションだけ止めて、灰色の板は残す */
  .sk {
    animation: none;
    background-image: none;
  }
}

動きを止めても、灰色の板そのものは残します。消してしまうと場所取りが効かなくなり、CLSが復活します。「動きを減らしたい」という要求であって「情報を減らしたい」ではありません。

2. スクリーンリーダーに灰色の板を読ませない

スケルトンは視覚的な予告であって、読み上げる内容を持ちません。装飾用の <div> が大量に読み上げられると、音声のユーザーは何が起きているかわからなくなります。

<!-- 読み込み中 -->
<div class="list" aria-busy="true">
  <p class="visually-hidden" role="status">記事を読み込んでいます</p>
  <div class="skeleton-card" aria-hidden="true"><!-- 中略なしで前述の構造 --></div>
  <div class="skeleton-card" aria-hidden="true"><!-- 同上 --></div>
</div>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
  • aria-hidden="true" でスケルトンの中身を読み上げ対象から外す
  • role="status" の要素に「読み込んでいます」という文字を置き、視覚的には隠す
  • 親に aria-busy="true" を付け、完了時に false に変える

role="status" は aria-live="polite" 相当なので、ユーザーの操作を遮らずに状態だけ伝えます。完了後は同じ要素のテキストを「記事を12件読み込みました」に差し替えると、音声だけで進行がわかります。

display: none や visibility: hidden で隠すとスクリーンリーダーも読まないため、必ず上記のクリップ方式を使います。

3. キーボードのフォーカスを迷子にしない

スケルトンから実物へ差し替わるとき、DOMを丸ごと置換する実装だと、フォーカスしていた要素が消えて <body> に飛びます。Tabキーで操作している人は、位置を見失って先頭からやり直すことになります。

対策は2つです。

  • スケルトン自体には tabindex を付けず、フォーカス不可のまま置く
  • 差し替え後、コンテナに tabindex="-1" を付けて focus() を当て、読み始める位置を明示する

無限スクロールの追加読み込みでは、既存のDOMを消さず末尾に追加するだけにします。そうすればフォーカスはどこへも飛びません。

コントラストは「背景との差」で考える

スケルトンは文字ではないので、WCAG の文字コントラスト比(4.5:1)はそのまま当てはまりません。ただし、板が背景とほとんど同じ色だと、ロービジョンのユーザーには何も表示されていないのと同じになります。

白背景(#ffffff)に対する目安です。

色 見え方 用途
#f1f3f5 かなり薄い。明るい環境では見えない 使わない
#e9ecef 標準。輪郭がはっきりわかる 推奨
#dee2e6 やや濃い。長時間だと重い 件数が少ないときだけ
#ced4da 濃い。中身が入ったように見えてしまう 使わない

実装したら、ディスプレイの輝度を半分に落として一度見てください。それで板の存在がわかるなら、屋外のスマホでも見えます。

色の選び方全般については、グレーの使い分けを整理したこちらも合わせてどうぞ。


表示タイミングの制御

速いときに出さない

APIが150ミリ秒で返ってくる環境でスケルトンを素直に出すと、灰色の板が一瞬表示されて即消えます。これは体感を悪くするだけです。

CSSだけで、出現を遅らせることができます。

.skeleton-card {
  animation: sk-appear 0s linear 0.25s forwards;
  opacity: 0;
}

@keyframes sk-appear {
  to {
    opacity: 1;
  }
}

animation-delay を 0.25秒にして、それまでは opacity: 0 のまま待たせます。250ミリ秒以内にデータが届いてスケルトンのDOMが消えれば、ユーザーは灰色の板を一度も見ません。JSで setTimeout を書く必要はありません。

遅延は 200〜300ミリ秒が扱いやすい範囲です。100ミリ秒だとほとんど効かず、500ミリ秒だと「何も起きない時間」が長くなって不安を与えます。

出したら最低時間は保持する

逆に、スケルトンが出た直後に消えるのも落ち着きません。表示したら最低300〜500ミリ秒は残す、という処理を入れると画面が安定します。こちらはCSSだけでは制御できないのでJS側で扱いますが、遅延表示だけでも大半のチラつきは消えるので、まずはCSSの1行から試してください。

スケルトン導入の作業手順
実物を先に組む完成形のHTML/CSSを確定させる
寸法を共有する高さ・aspect-ratioを同じクラスで持つ
板を置く色と角丸だけの静止版を作る
動きを足すシマーかパルスを1種類だけ付ける
止める条件を書くreduced-motionと遅延表示を入れる

スケルトンを先に作ると寸法が合いません。必ず実物を確定させてから、その寸法を写す順番で進めます。


案件の種類ごとの判断

「どれも良い」で終わらせないために、使うかどうかを分けて書きます。

入れるべき

記事一覧・商品一覧・検索結果ページ。繰り返し構造が決まっていて、外部データを待つ画面です。ここは最も効きます。1件分のスケルトンを作り、表示予定の件数だけ並べます。

ダッシュボード・管理画面。数値カードやグラフが複数ブロック並ぶ画面は、どこから埋まるかが見えるだけで待ち時間の体感が変わります。グラフ領域は矩形の板を1枚置けば十分で、折れ線の形まで真似る必要はありません。

SNS風のタイムライン。無限スクロールの末尾に2〜3件分だけ置くのが定石です。

入れなくてよい

静的なコーポレートサイト・LP。HTMLが一度に返ってくるページに、JSで後からスケルトンを出す理由がありません。むしろJSの読み込み分だけ表示が遅れます。ここで効くのは画像の width/height 指定とフォントの font-display 調整です。

フォームの送信処理。結果が1点なので、ボタン内のスピナーと disabled のほうが明快です。

管理画面の設定ページなど、項目数が毎回変わる画面。何枚の板を並べればいいか決まらず、ズレが出ます。素直にスピナーを置きます。

入れると逆効果

1〜2秒で終わらない重い処理。灰色の板が点滅し続ける画面は、壊れているように見えます。進捗表示に切り替えます。

スケルトンの形が実物と違うまま出すケース。これは繰り返しになりますが、入れないほうがマシです。寸法を合わせられないなら、最初から作らない判断が正解です。

CSSで表現を足していく実装の考え方としては、ノイズやテクスチャの記事も同じ流れで読めます。


コピーして使える完成形

ここまでの条件を全部盛り込んだ、そのまま動くコードです。HTMLとCSSを貼れば、記事カード3枚分のスケルトンが表示されます。

<section class="sk-list" aria-busy="true">
  <p class="visually-hidden" role="status">記事を読み込んでいます</p>

  <article class="sk-card" aria-hidden="true">
    <div class="sk sk-thumb"></div>
    <div class="sk-body">
      <div class="sk sk-meta"></div>
      <div class="sk sk-title"></div>
      <div class="sk sk-text"></div>
      <div class="sk sk-text sk-text--short"></div>
    </div>
  </article>

  <article class="sk-card" aria-hidden="true">
    <div class="sk sk-thumb"></div>
    <div class="sk-body">
      <div class="sk sk-meta"></div>
      <div class="sk sk-title"></div>
      <div class="sk sk-text"></div>
      <div class="sk sk-text sk-text--short"></div>
    </div>
  </article>

  <article class="sk-card" aria-hidden="true">
    <div class="sk sk-thumb"></div>
    <div class="sk-body">
      <div class="sk sk-meta"></div>
      <div class="sk sk-title"></div>
      <div class="sk sk-text"></div>
      <div class="sk sk-text sk-text--short"></div>
    </div>
  </article>
</section>
:root {
  --sk-base: #e9ecef;
  --sk-shine: rgba(255, 255, 255, 0.65);
  --sk-radius: 6px;
  --sk-speed: 1.4s;
  --sk-delay: 0.25s;
}

.sk-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
  max-width: 680px;
  margin: 0 auto;
  padding: 24px 16px;
}

.sk-card {
  display: flex;
  gap: 16px;
  padding: 16px;
  background: #fff;
  border: 1px solid #eef0f2;
  border-radius: 12px;
  opacity: 0;
  animation: sk-appear 0s linear var(--sk-delay) forwards;
}

.sk-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-top: 2px;
  min-width: 0;
}

.sk {
  background-color: var(--sk-base);
  background-image: linear-gradient(
    100deg,
    rgba(255, 255, 255, 0) 20%,
    var(--sk-shine) 50%,
    rgba(255, 255, 255, 0) 80%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  background-position: -120% 0;
  border-radius: var(--sk-radius);
  animation: sk-shimmer var(--sk-speed) ease-in-out infinite;
}

.sk-thumb {
  flex: 0 0 128px;
  aspect-ratio: 4 / 3;
  border-radius: 8px;
}

.sk-meta {
  height: 11px;
  width: 30%;
}

.sk-title {
  height: 19px;
  width: 72%;
}

.sk-text {
  height: 13px;
  width: 100%;
}

.sk-text--short {
  width: 48%;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@keyframes sk-shimmer {
  to {
    background-position: 220% 0;
  }
}

@keyframes sk-appear {
  to {
    opacity: 1;
  }
}

@media (max-width: 600px) {
  .sk-thumb {
    flex-basis: 96px;
  }
  .sk-title {
    height: 17px;
    width: 86%;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --sk-base: #2a2d31;
    --sk-shine: rgba(255, 255, 255, 0.08);
  }
  .sk-card {
    background: #1c1f22;
    border-color: #2a2d31;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sk {
    animation: none;
    background-image: none;
  }
  .sk-card {
    opacity: 1;
    animation: none;
  }
}

使っているCSS機能のうち、注意が要るのは3つです。

  • aspect-ratio: 主要ブラウザで利用できるが、古い環境も対象なら padding-top 方式に差し替える
  • clip-path: inset(50%): 広く使えるが、対象ブラウザが古い案件では clip: rect(0 0 0 0) を併記する
  • カスタムプロパティ(--sk-base など): IEを対象外にできる案件なら問題なし

いずれも、対象ブラウザが決まったらMDNのBrowser compatibility表かcaniuseで一度確認してから採用してください。「たぶん動く」で入れると、検証端末で崩れます。

動的に件数を変える場合

表示予定の件数が決まっているなら、その数だけ並べます。わからない場合は 3枚が無難です。1枚だと寂しく、画面を埋め尽くすほど並べると、実際に1件しか返ってこなかったときの落差が大きくなります。


よくある質問

スケルトンスクリーンは本当に体感速度を上げますか

読み込み自体は1ミリ秒も速くなりません。変わるのは、待っている間に何が起きているかが伝わるかどうかです。完了後の構造が予告されるため、データが届いた瞬間に読み始められます。効果が出るのは0.3〜3秒程度の待ち時間で、それより短いと見えず、長いと逆に不安を与えます。

スケルトンはJavaScriptなしで実装できますか

見た目はCSSだけで完全に作れます。表示の遅延も animation-delay で制御できます。ただし「データが届いたらスケルトンを消して実物に差し替える」部分はJS(またはサーバー側のレンダリング)が担当します。CSSだけで完結するのは見た目と出現タイミングまでです。

光が流れるアニメーションは必須ですか

必須ではありません。灰色の板を置くだけでもCLS防止という本来の目的は達成できます。シマーは「処理が進んでいる」ことを伝える補助で、カードが30枚以上並ぶ一覧では描画コストが積み上がるので、opacity を呼吸させるパルス方式に切り替えるか、動きなしにするほうが実用的です。

スケルトンの色は何色にすればいいですか

白背景なら #e9ecef を起点にします。これより薄いと明るい環境で見えず、濃いと中身が入っているように誤認されます。ダークテーマでは #2a2d31 前後にして、流す光の不透明度を 0.06〜0.10 まで落とします。ライトと同じ強さの白を暗い画面に流すと目に刺さります。

無限スクロールでもスケルトンを使っていいですか

使えます。むしろ相性が良い場面です。既存のDOMを消さず、末尾に2〜3件分だけ追加する形にします。リスト全体を置き換える実装にすると、スクロール位置とキーボードのフォーカスが飛ぶので避けてください。


まとめ

スケルトンスクリーンは待ち時間を短くする魔法ではなく、完了後のレイアウトを先に確定させておく場所取りです。この一点を外さなければ、実装の判断はほとんど迷いません。

実装時に必ず押さえる5点をまとめます。

  1. 寸法は実物と共有する。合わせられないなら作らない
  2. 灰色は背景と区別がつく濃さにする(白背景なら #e9ecef 前後)
  3. prefers-reduced-motion で動きを止める。板は残す
  4. aria-hidden と role="status" で、音声には文字で伝える
  5. animation-delay で200〜300ミリ秒遅らせ、速いときは出さない

重いフィルターや画像を使わず、background-position か opacity の範囲で収める。これを守れば、カードが並ぶ一覧でも描画が詰まりません。

CSSだけで表現を積み上げる実装は、他の様式にも共通の考え方があります。