スケルトンローディング
読み込み中に輝く光が流れるプレースホルダを表示し、完了後に実コンテンツへ差し替わる。
プレビュー
操作クリック/タップで動きます。
仕様書
- 向く場面
- 読み込み後の形が決まっているカード・一覧
- レイアウトシフトを防ぎたい非同期表示
- 0.5 秒以上かかる読み込み
- 避ける場面
- 形が読み込むまで分からない内容
- 0.3 秒未満で終わる読み込み
- ボタン 1 個の処理中表示
- 構成要素
- 骨組みカード
- アバターの円
- テキストの帯
- 実カード
- 再読み込みボタン
- 持っている状態
- 通常
- ホバー
- 無効
- 読み込み中
- キーボード
Tab で「再読み込み」へ、Enter/Space で再読込。読み込み中の 2.2 秒は disabled
- 支援技術
骨組みは文字を持たない div で読み上げは無音。包む要素に aria-busy を立て、読み込み中はボタンが disabled。差し替えは hidden 属性。reduced-motion で光の帯を止める
- 動き
光の帯 1.4s ease-in-out 無限(translateX −100%→100%)。ボタン hover 150ms。reduced-motion で静止
デザインの要点
- 骨組みは実カードと同じ構造(円 44px+帯 2 本)にする。寸法が合うほど差し替え時のガタつきが小さい
- 光の帯は ::after の transform だけを動かす。背景位置を動かすより軽く、帯を何本並べても引っかからない
- 帯の 2 本目は 65% 幅にする。全部同じ長さだと「本文が載る場所」に見えず、ただの棒に見える
- hidden 属性でクラスの display:flex に勝つには [hidden]{display:none} を明示する。忘れると骨組みが消えない
使いどころ
読み込み後の見た目がある程度予測できる場面に向く。カード一覧・プロフィール・記事一覧のように、事前にレイアウト(アバターの円・見出しの帯・本文の帯)が決まっているコンテンツなら、実データの形に合わせたグレーの帯を先に置くことで、読み込み中でも画面がガタつかず「もうすぐこの形の情報が出る」と予測させられる。スピナーと違って最終レイアウトの寸法をあらかじめ占有するため、読み込み完了時のレイアウトシフトも防げる。逆に、最終的な形が読み込むまで分からないコンテンツ(件数や画像の縦横比が毎回変わる一覧)や、0.3〜0.5秒未満で終わる短い読み込みには向かない。形の合わない骨組みを一瞬見せるとちらつきの方が目立ち、スピナーか何も出さない方が自然になる。単発のボタン処理や小さいアイコン1個の差し替えのような形を持たない読み込みには、スケルトンではなくスピナーを使う方がよい。
仕組み
.sk要素にposition:relative; overflow:hiddenを与え、::afterでlinear-gradient(透明→白50%→透明)の帯をtransform:translateX(-100%)からtranslateX(100%)へ1.4秒かけて流すことで光沢(シマー)を作る。transformだけを動かしているためコンポジタで処理でき、帯を何個並べても軽い。骨組みの構造(アバターの円+見出し2行)は実カードのDOM構造をそのまま模しており、幅(長い行100%/中くらいの行65%)や高さを実コンテンツの寸法に近づけるほど差し替え時の見た目のジャンプが小さくなる。表示切替はhidden属性で行うが、.skeleton-cardや.real-cardはクラスでdisplay:flexを指定しているため、hidden属性だけでは隠れない。実装ではhidden時にdisplay:noneを明示的に上書きして、属性セレクタの詳細度でクラスのdisplay指定に勝たせている。この上書きを忘れると、hiddenを付けても要素がflexのまま画面に残ってしまう。
実装の注意
最大の罠は上記のhidden属性とdisplay:flexの詳細度の衝突で、これを見落とすとJSでhiddenを切り替えても骨組みが消えない。次に、このサンプルの読み込みはsetTimeout(2200)の固定待ちだが、実装では実際のfetch完了に連動させる必要があり、その際「速く返ってきた場合」への配慮が抜けやすい。データが200ms未満で返るとスケルトンが一瞬だけ点滅してかえってちらついて見えるため、表示するかどうか自体に遅延を入れる(例えば200ms待ってまだ読み込み中ならスケルトンを出す)か、出したら最低500ms程度は表示を保つ実装にするとよい。また、骨組みのマークアップと実カードのマークアップを別々に手書きしているため、実コンテンツのレイアウトを変更したときに骨組み側の更新を忘れやすく、寸法がずれて差し替え時にガクッと動く原因になる。可能ならCSSの寸法値を共有の変数にしておくと乖離を防げる。
アクセシビリティ
このサンプルの骨組み要素は文字を持たないdivのみなので、スクリーンリーダーには何も読み上げられない。読み込み中であることは、骨組みと実カードを包む親要素(.card-wrap)のaria-busy=trueで支援技術に伝え、実コンテンツへの差し替えと同時にaria-busy=falseへ戻している。読み込み完了を音声で知らせたい場合は、親要素とは別に用意したaria-live=polite領域に「読み込みました」のような短い文言を差し込む(骨組み自体をlive領域にすると、生成・削除のたびに空の変化が読み上げられかねないため分離した方が安全)。シマーアニメーションはprefers-reduced-motion: reduceで.sk::afterのanimationを止め、静的なグレーの帯だけを見せる(同じ辞典のローディングスピナーと同じ方針)。動きが無くてもaria-busyが「読み込み中」の意味を担うので情報は失われない。
調整して使う
変えられる値は主に3つ。帯の速度(1.4秒が既定。遅くすると上品だが動いている感が弱まり、0.9〜1.6秒が実用域)、骨組みの配色(明るい背景ではベージュ系、ダークテーマでは暗いグレーに薄い白の帯)、そして骨組みの粒度である。今回はアバター+2行だが、画像ブロックやボタン形の.skを追加すれば、より複雑なカード(サムネイル付き記事、商品カードなど)にも同じ手法で対応できる。一覧表示では複数枚の骨組みを縦に並べ、各カードのanimation-delayを少しずつずらす(0.1秒刻みなど)と、光が上から下へ流れるように見えて単調さが減る。シマーの帯を流す代わりに、骨組み全体をopacityで明滅させるパルス型に差し替える設計もあり、帯より落ち着いた印象になるぶん動いて読み込み中だという手がかりはやや弱まる。
コード(コピーして使えます)
<div class="card-wrap" id="cardWrap" aria-busy="true">
<div class="skeleton-card" id="skel">
<div class="sk sk-avatar"></div>
<div class="sk-lines">
<div class="sk sk-line sk-line--long"></div>
<div class="sk sk-line sk-line--mid"></div>
</div>
</div>
<div class="real-card" id="real" hidden>
<div class="real-avatar">🧑💻</div>
<div class="real-lines">
<p class="real-name">田中 太郎</p>
<p class="real-sub">フロントエンドエンジニア</p>
</div>
</div>
<button class="reload-btn" id="reload">再読み込み</button>
</div>