スケルトン→表示遷移
シマー付きスケルトンから実コンテンツへなめらかに切り替えるロード演出。体感速度の向上に役立ちます。
ライブデモ
使用例(お題: カフェ MOON BREW)
この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- MOON BREW:メニュー読込のスケルトン→表示遷移 -->
<section class="sk-stage">
<header class="sk-head">
<div class="sk-brand"><span class="sk-cup">☕</span> MOON BREW</div>
<span class="sk-sub">本日のメニュー</span>
</header>
<ul class="sk-list" id="skList">
<li class="sk-row is-loading">
<div class="sk-thumb"></div>
<div class="sk-info">
<div class="sk-line sk-line--title"></div>
<div class="sk-line sk-line--text"></div>
</div>
<div class="sk-price"></div>
<!-- 実コンテンツ(最初は非表示) -->
<img class="sk-real-thumb" src="https://picsum.photos/120/120?random=51" alt="">
<div class="sk-real-info"><p class="sk-name">焙煎ハニーラテ</p><p class="sk-note">深煎り × 信州はちみつ</p></div>
<span class="sk-real-price">¥620</span>
</li>
<li class="sk-row is-loading">
<div class="sk-thumb"></div>
<div class="sk-info">
<div class="sk-line sk-line--title"></div>
<div class="sk-line sk-line--text"></div>
</div>
<div class="sk-price"></div>
<img class="sk-real-thumb" src="https://picsum.photos/120/120?random=52" alt="">
<div class="sk-real-info"><p class="sk-name">月見カフェオレ</p><p class="sk-note">まろやか泡立てミルク</p></div>
<span class="sk-real-price">¥580</span>
</li>
<li class="sk-row is-loading">
<div class="sk-thumb"></div>
<div class="sk-info">
<div class="sk-line sk-line--title"></div>
<div class="sk-line sk-line--text"></div>
</div>
<div class="sk-price"></div>
<img class="sk-real-thumb" src="https://picsum.photos/120/120?random=53" alt="">
<div class="sk-real-info"><p class="sk-name">ドリップ・本日の豆</p><p class="sk-note">エチオピア ナチュラル</p></div>
<span class="sk-real-price">¥520</span>
</li>
</ul>
<button class="sk-btn" id="skBtn" type="button">⟳ 再読み込み</button>
</section>
CSS
/* MOON BREW:メニューのスケルトン→実表示 */
:root {
--cream: #f5ede1;
--brown: #2b1d12;
--amber: #c98a3b;
}
* { box-sizing: border-box; }
body {
margin: 0;
height: 400px;
font-family: "Hiragino Mincho ProN", "Yu Mincho", "Segoe UI", serif;
background: var(--cream);
color: var(--brown);
}
.sk-stage { height: 400px; padding: 18px 22px; display: flex; flex-direction: column; }
.sk-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.sk-brand { font-size: 16px; font-weight: 800; letter-spacing: 0.08em; }
.sk-cup { font-size: 14px; }
.sk-sub { font-size: 11px; letter-spacing: 0.1em; color: #8a7256; }
.sk-list { list-style: none; margin: 0; padding: 0; flex: 1; display: grid; gap: 10px; align-content: start; }
/* 各行 */
.sk-row {
position: relative;
display: flex;
align-items: center;
gap: 14px;
padding: 12px;
border-radius: 14px;
background: #fff;
border: 1px solid rgba(43, 29, 18, 0.08);
box-shadow: 0 8px 20px -14px rgba(43, 29, 18, 0.4);
min-height: 72px;
}
/* --- スケルトン側(読込中だけ表示) --- */
.sk-thumb, .sk-line, .sk-price {
background: linear-gradient(100deg, #e9ddcb 30%, #f5ede1 50%, #e9ddcb 70%);
background-size: 220% 100%;
border-radius: 8px;
animation: sk-shimmer 1.3s ease-in-out infinite;
}
.sk-thumb { flex: none; width: 48px; height: 48px; border-radius: 10px; }
.sk-info { flex: 1; display: grid; gap: 8px; }
.sk-line--title { height: 13px; width: 60%; }
.sk-line--text { height: 10px; width: 85%; }
.sk-price { flex: none; width: 44px; height: 16px; }
@keyframes sk-shimmer {
to { background-position: -120% 0; }
}
/* --- 実コンテンツ側(最初は非表示で重ねておく) --- */
.sk-real-thumb, .sk-real-info, .sk-real-price {
opacity: 0;
transform: translateY(6px);
transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.sk-real-thumb { flex: none; width: 48px; height: 48px; border-radius: 10px; object-fit: cover; }
.sk-real-info { flex: 1; }
.sk-name { margin: 0 0 4px; font-size: 14px; font-weight: 700; }
.sk-note { margin: 0; font-size: 11px; color: #8a7256; }
.sk-real-price { flex: none; font-size: 15px; font-weight: 800; color: var(--amber); }
/* 読込中はスケルトンのみ/実コンテンツは place を占めない */
.sk-row.is-loading .sk-real-thumb,
.sk-row.is-loading .sk-real-info,
.sk-row.is-loading .sk-real-price { display: none; }
/* 読込完了:スケルトンを隠し実コンテンツをフェードイン */
.sk-row.is-ready .sk-thumb,
.sk-row.is-ready .sk-info,
.sk-row.is-ready .sk-price { display: none; }
.sk-row.is-ready .sk-real-thumb,
.sk-row.is-ready .sk-real-info,
.sk-row.is-ready .sk-real-price { opacity: 1; transform: translateY(0); }
.sk-btn {
margin-top: 14px;
width: 100%;
font-family: inherit; font-size: 12px; font-weight: 700;
padding: 10px; border: 1px solid rgba(43, 29, 18, 0.18); border-radius: 10px;
background: transparent; color: var(--brown); cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.sk-btn:hover { background: rgba(201, 138, 59, 0.12); }
.sk-btn:active { transform: scale(0.98); }
@media (prefers-reduced-motion: reduce) {
.sk-thumb, .sk-line, .sk-price { animation: none; }
.sk-real-thumb, .sk-real-info, .sk-real-price { transition: none; }
}
JavaScript
// MOON BREW:スケルトンから実メニューへ時間差で切り替える
(() => {
const list = document.getElementById("skList");
const btn = document.getElementById("skBtn");
if (!list) return; // null安全
const rows = Array.from(list.querySelectorAll(".sk-row"));
let timers = [];
// すべて読込状態に戻す
const reset = () => {
timers.forEach((t) => clearTimeout(t));
timers = [];
rows.forEach((r) => {
r.classList.remove("is-ready");
r.classList.add("is-loading");
});
};
// 行ごとに時間差で実表示へ
const load = () => {
reset();
rows.forEach((r, i) => {
const t = setTimeout(() => {
r.classList.remove("is-loading");
r.classList.add("is-ready");
}, 700 + i * 320);
timers.push(t);
});
};
load(); // 初回ロード
if (btn) btn.addEventListener("click", load);
})();
コード
HTML
<!-- スケルトン→表示遷移:ロード中のプレースホルダから実コンテンツへ -->
<div class="skel-stage">
<article class="skel-card" id="skelCard" data-state="loading">
<!-- スケルトン層 -->
<div class="skel-layer skel-ghost" aria-hidden="true">
<div class="sk sk-avatar"></div>
<div class="sk-lines">
<div class="sk sk-line w70"></div>
<div class="sk sk-line w40"></div>
</div>
<div class="sk sk-media"></div>
<div class="sk sk-line w90"></div>
<div class="sk sk-line w60"></div>
</div>
<!-- 実コンテンツ層(最初は不可視) -->
<div class="skel-layer skel-real">
<div class="skel-head">
<div class="skel-avatar">M</div>
<div>
<p class="skel-name">Mika Tanaka</p>
<p class="skel-meta">UIデザイナー・2分前</p>
</div>
</div>
<img class="skel-img" src="https://picsum.photos/seed/skelreveal/480/200" alt="サンプル画像" loading="lazy">
<p class="skel-text">新しいモーションガイドラインを公開しました。読み込み中はスケルトンで構造を示し、完了後になめらかに切り替えます。</p>
</div>
</article>
<button class="skel-btn" id="skelReload" type="button">↻ 再読み込み</button>
</div>
CSS
/* 明るいカードUI。2層を重ねて状態で切替える */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
background: linear-gradient(160deg, #eef1f8 0%, #dfe4f2 100%);
color: #2a2f45;
}
.skel-stage { display: grid; gap: 16px; justify-items: center; width: 100%; }
.skel-card {
position: relative;
width: min(320px, 86vw);
min-height: 240px;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 18px 44px -18px rgba(40, 50, 110, .4);
overflow: hidden;
}
/* 2層を同じ場所に重ねる */
.skel-layer { display: grid; gap: 12px; }
.skel-real {
position: absolute; inset: 16px;
opacity: 0;
transform: translateY(8px);
transition: opacity .5s ease, transform .5s cubic-bezier(.22,1,.36,1);
pointer-events: none;
}
/* 状態:ロード完了でゴーストを消し実体を出す */
.skel-card[data-state="ready"] .skel-ghost { opacity: 0; pointer-events: none; }
.skel-card[data-state="ready"] .skel-real { opacity: 1; transform: translateY(0); pointer-events: auto; }
.skel-ghost { transition: opacity .35s ease; }
/* スケルトン部品:シマー(光沢)を流す */
.sk {
position: relative;
border-radius: 8px;
background: #e9edf6;
overflow: hidden;
}
.sk::after {
content: "";
position: absolute; inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,.85), transparent);
animation: skShimmer 1.3s ease-in-out infinite;
}
@keyframes skShimmer { 100% { transform: translateX(100%); } }
.sk-avatar { width: 44px; height: 44px; border-radius: 50%; }
.sk-lines { display: grid; gap: 8px; }
.sk-line { height: 11px; }
.sk-media { height: 110px; border-radius: 10px; }
.w40 { width: 40%; } .w60 { width: 60%; } .w70 { width: 70%; } .w90 { width: 90%; }
/* アバターと2行を横並びに */
.skel-ghost { grid-template-columns: auto 1fr; align-items: center; }
.skel-ghost .sk-media,
.skel-ghost .sk-line:not(.sk-lines .sk-line) { grid-column: 1 / -1; }
.skel-ghost > .sk-media { grid-column: 1 / -1; }
.skel-ghost > .sk-line { grid-column: 1 / -1; }
/* 実コンテンツ */
.skel-head { display: flex; align-items: center; gap: 12px; }
.skel-avatar {
width: 44px; height: 44px; border-radius: 50%;
display: grid; place-items: center;
font-weight: 800; color: #fff;
background: linear-gradient(135deg, #7c8bff, #39d3ff);
}
.skel-name { margin: 0; font-size: 14px; font-weight: 700; }
.skel-meta { margin: 2px 0 0; font-size: 12px; color: #8b91ab; }
.skel-img { width: 100%; height: 110px; object-fit: cover; border-radius: 10px; display: block; }
.skel-text { margin: 0; font-size: 13px; line-height: 1.6; color: #4a5070; }
.skel-btn {
padding: 9px 18px;
border: 1px solid rgba(60, 70, 130, .25);
border-radius: 10px;
background: #fff; color: #3a4276;
font-size: 13px; font-weight: 600; cursor: pointer;
box-shadow: 0 6px 16px -8px rgba(40, 50, 110, .5);
}
.skel-btn:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
.sk::after { animation: none; }
.skel-real { transition: opacity .2s ease; transform: none; }
}
JavaScript
// スケルトン→表示:擬似ロード後に data-state を ready へ切り替える
(() => {
const card = document.getElementById('skelCard');
const reload = document.getElementById('skelReload');
if (!card) return; // null安全
let timer = null;
const LOAD_MS = 1800; // 擬似的なロード時間
const startLoading = () => {
if (timer) clearTimeout(timer);
card.dataset.state = 'loading';
timer = setTimeout(() => { card.dataset.state = 'ready'; }, LOAD_MS);
};
// 初回ロード演出
startLoading();
if (reload) reload.addEventListener('click', startLoading);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スケルトン→表示遷移」の効果を追加してください。
# 追加してほしい効果
スケルトン→表示遷移(アニメーション & トランジション)
シマー付きスケルトンから実コンテンツへなめらかに切り替えるロード演出。体感速度の向上に役立ちます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- スケルトン→表示遷移:ロード中のプレースホルダから実コンテンツへ -->
<div class="skel-stage">
<article class="skel-card" id="skelCard" data-state="loading">
<!-- スケルトン層 -->
<div class="skel-layer skel-ghost" aria-hidden="true">
<div class="sk sk-avatar"></div>
<div class="sk-lines">
<div class="sk sk-line w70"></div>
<div class="sk sk-line w40"></div>
</div>
<div class="sk sk-media"></div>
<div class="sk sk-line w90"></div>
<div class="sk sk-line w60"></div>
</div>
<!-- 実コンテンツ層(最初は不可視) -->
<div class="skel-layer skel-real">
<div class="skel-head">
<div class="skel-avatar">M</div>
<div>
<p class="skel-name">Mika Tanaka</p>
<p class="skel-meta">UIデザイナー・2分前</p>
</div>
</div>
<img class="skel-img" src="https://picsum.photos/seed/skelreveal/480/200" alt="サンプル画像" loading="lazy">
<p class="skel-text">新しいモーションガイドラインを公開しました。読み込み中はスケルトンで構造を示し、完了後になめらかに切り替えます。</p>
</div>
</article>
<button class="skel-btn" id="skelReload" type="button">↻ 再読み込み</button>
</div>
【CSS】
/* 明るいカードUI。2層を重ねて状態で切替える */
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 360px;
display: grid;
place-items: center;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic UI", system-ui, sans-serif;
background: linear-gradient(160deg, #eef1f8 0%, #dfe4f2 100%);
color: #2a2f45;
}
.skel-stage { display: grid; gap: 16px; justify-items: center; width: 100%; }
.skel-card {
position: relative;
width: min(320px, 86vw);
min-height: 240px;
padding: 16px;
border-radius: 16px;
background: #fff;
box-shadow: 0 18px 44px -18px rgba(40, 50, 110, .4);
overflow: hidden;
}
/* 2層を同じ場所に重ねる */
.skel-layer { display: grid; gap: 12px; }
.skel-real {
position: absolute; inset: 16px;
opacity: 0;
transform: translateY(8px);
transition: opacity .5s ease, transform .5s cubic-bezier(.22,1,.36,1);
pointer-events: none;
}
/* 状態:ロード完了でゴーストを消し実体を出す */
.skel-card[data-state="ready"] .skel-ghost { opacity: 0; pointer-events: none; }
.skel-card[data-state="ready"] .skel-real { opacity: 1; transform: translateY(0); pointer-events: auto; }
.skel-ghost { transition: opacity .35s ease; }
/* スケルトン部品:シマー(光沢)を流す */
.sk {
position: relative;
border-radius: 8px;
background: #e9edf6;
overflow: hidden;
}
.sk::after {
content: "";
position: absolute; inset: 0;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, rgba(255,255,255,.85), transparent);
animation: skShimmer 1.3s ease-in-out infinite;
}
@keyframes skShimmer { 100% { transform: translateX(100%); } }
.sk-avatar { width: 44px; height: 44px; border-radius: 50%; }
.sk-lines { display: grid; gap: 8px; }
.sk-line { height: 11px; }
.sk-media { height: 110px; border-radius: 10px; }
.w40 { width: 40%; } .w60 { width: 60%; } .w70 { width: 70%; } .w90 { width: 90%; }
/* アバターと2行を横並びに */
.skel-ghost { grid-template-columns: auto 1fr; align-items: center; }
.skel-ghost .sk-media,
.skel-ghost .sk-line:not(.sk-lines .sk-line) { grid-column: 1 / -1; }
.skel-ghost > .sk-media { grid-column: 1 / -1; }
.skel-ghost > .sk-line { grid-column: 1 / -1; }
/* 実コンテンツ */
.skel-head { display: flex; align-items: center; gap: 12px; }
.skel-avatar {
width: 44px; height: 44px; border-radius: 50%;
display: grid; place-items: center;
font-weight: 800; color: #fff;
background: linear-gradient(135deg, #7c8bff, #39d3ff);
}
.skel-name { margin: 0; font-size: 14px; font-weight: 700; }
.skel-meta { margin: 2px 0 0; font-size: 12px; color: #8b91ab; }
.skel-img { width: 100%; height: 110px; object-fit: cover; border-radius: 10px; display: block; }
.skel-text { margin: 0; font-size: 13px; line-height: 1.6; color: #4a5070; }
.skel-btn {
padding: 9px 18px;
border: 1px solid rgba(60, 70, 130, .25);
border-radius: 10px;
background: #fff; color: #3a4276;
font-size: 13px; font-weight: 600; cursor: pointer;
box-shadow: 0 6px 16px -8px rgba(40, 50, 110, .5);
}
.skel-btn:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
.sk::after { animation: none; }
.skel-real { transition: opacity .2s ease; transform: none; }
}
【JavaScript】
// スケルトン→表示:擬似ロード後に data-state を ready へ切り替える
(() => {
const card = document.getElementById('skelCard');
const reload = document.getElementById('skelReload');
if (!card) return; // null安全
let timer = null;
const LOAD_MS = 1800; // 擬似的なロード時間
const startLoading = () => {
if (timer) clearTimeout(timer);
card.dataset.state = 'loading';
timer = setTimeout(() => { card.dataset.state = 'ready'; }, LOAD_MS);
};
// 初回ロード演出
startLoading();
if (reload) reload.addEventListener('click', startLoading);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。