スケルトン→表示遷移

シマー付きスケルトンから実コンテンツへなめらかに切り替えるロード演出。体感速度の向上に役立ちます。

#css#javascript#skeleton#loading

ライブデモ

使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。