staggered リスト出現

リスト各行を時間差でフェード&スライドインさせる演出。タスク一覧や検索結果の登場アニメーションに使えます。

#css#animation#stagger#list

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:検索結果が時間差で出現するタスク一覧画面 -->
<section class="fd-stage">
  <header class="fd-top">
    <div class="fd-brand"><span class="fd-mark">◆</span> FlowDesk</div>
    <div class="fd-search">
      <span class="fd-search__ico">⌕</span>
      <span class="fd-search__q">「請求」で検索</span>
      <span class="fd-search__count" id="fdCount">4件</span>
    </div>
  </header>

  <ul class="fd-list" id="fdList">
    <li class="fd-row">
      <span class="fd-ico fd-ico--blue">⬡</span>
      <div class="fd-body">
        <p class="fd-name">10月分の請求書を送付</p>
        <p class="fd-meta">経理タスク · 期限 今日</p>
      </div>
      <span class="fd-tag fd-tag--due">急ぎ</span>
    </li>
    <li class="fd-row">
      <span class="fd-ico fd-ico--blue">⬡</span>
      <div class="fd-body">
        <p class="fd-name">請求フローの自動化を確認</p>
        <p class="fd-meta">ワークフロー · 担当 田中</p>
      </div>
      <span class="fd-tag">進行中</span>
    </li>
    <li class="fd-row">
      <span class="fd-ico fd-ico--blue">⬡</span>
      <div class="fd-body">
        <p class="fd-name">請求テンプレートを更新</p>
        <p class="fd-meta">ドキュメント · 更新 3日前</p>
      </div>
      <span class="fd-tag">完了</span>
    </li>
    <li class="fd-row">
      <span class="fd-ico fd-ico--blue">⬡</span>
      <div class="fd-body">
        <p class="fd-name">未請求の顧客をレビュー</p>
        <p class="fd-meta">レポート · 担当 佐藤</p>
      </div>
      <span class="fd-tag">未着手</span>
    </li>
  </ul>

  <button class="fd-replay" id="fdReplay" type="button">⟳ 検索をやり直す</button>
</section>
CSS
/* FlowDesk:検索結果リストの staggered 出現 */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(120% 120% at 85% 0%, #18294b 0%, #0f1b34 60%, #0b1428 100%);
  color: #eef2ff;
}

.fd-stage {
  height: 400px;
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
}

/* 上部バー */
.fd-top { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.fd-brand { font-size: 14px; font-weight: 800; letter-spacing: 0.04em; white-space: nowrap; }
.fd-mark { color: var(--blue); }

.fd-search {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 12px;
}
.fd-search__ico { color: #9db4ff; }
.fd-search__q { color: rgba(255, 255, 255, 0.85); }
.fd-search__count {
  margin-left: auto;
  font-size: 11px;
  color: #9db4ff;
  background: rgba(79, 124, 255, 0.18);
  padding: 2px 8px;
  border-radius: 999px;
}

/* 結果リスト */
.fd-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

/* 各行:初期は不可視+少し下、JSで is-in を付与 */
.fd-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 13px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
.fd-row.is-in { opacity: 1; transform: translate3d(0, 0, 0); }

.fd-ico {
  flex: none;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 8px;
  font-size: 13px;
}
.fd-ico--blue { background: rgba(79, 124, 255, 0.18); color: #9db4ff; }

.fd-body { flex: 1; min-width: 0; }
.fd-name { margin: 0 0 2px; font-size: 13px; font-weight: 600; }
.fd-meta { margin: 0; font-size: 11px; color: rgba(255, 255, 255, 0.55); }

.fd-tag {
  flex: none;
  font-size: 10px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.8);
}
.fd-tag--due { background: rgba(255, 99, 99, 0.18); color: #ff9b9b; }

.fd-replay {
  margin-top: auto;
  width: 100%;
  font: inherit;
  font-size: 12px;
  padding: 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 10px;
  background: rgba(79, 124, 255, 0.16);
  color: #cdd8ff;
  cursor: pointer;
  transition: background 0.2s ease, transform 0.1s ease;
}
.fd-replay:hover { background: rgba(79, 124, 255, 0.3); }
.fd-replay:active { transform: scale(0.98); }

@media (prefers-reduced-motion: reduce) {
  .fd-row { transition: none; opacity: 1; transform: none; }
}
JavaScript
// FlowDesk:検索結果の各行を時間差で表示する
(() => {
  const list = document.getElementById("fdList");
  const replay = document.getElementById("fdReplay");
  if (!list) return; // null安全

  const rows = Array.from(list.querySelectorAll(".fd-row"));
  const STEP = 100; // 1行ごとの遅延(ms)

  // 全消ししてから時間差で再表示
  const play = () => {
    rows.forEach((el) => el.classList.remove("is-in"));
    rows.forEach((el, i) => {
      setTimeout(() => el.classList.add("is-in"), 120 + i * STEP);
    });
  };

  play(); // 初回再生
  if (replay) replay.addEventListener("click", play);
})();

コード

HTML
<!-- staggered リスト出現デモ:各行が時間差でフェード&スライドイン -->
<div class="stagger-stage">
  <div class="stagger-card">
    <header class="stagger-head">
      <span class="stagger-dot"></span>
      <h2 class="stagger-title">Today's Tasks</h2>
    </header>
    <ul class="stagger-list" id="staggerList">
      <li class="stagger-item"><span class="stagger-ico">◆</span> 朝のストレッチ</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> メールを確認</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> デザインレビュー</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> ランチ休憩</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> プロト実装</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> 振り返りメモ</li>
    </ul>
    <button class="stagger-replay" id="staggerReplay" type="button">▶ もう一度再生</button>
  </div>
</div>
CSS
/* 全体の下地:落ち着いたグラデ背景で中央寄せ */
* { 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: radial-gradient(120% 120% at 20% 0%, #2b2d6e 0%, #14152e 55%, #0b0b1a 100%);
  color: #f4f5ff;
}
.stagger-stage { padding: 24px; width: 100%; display: grid; place-items: center; }

/* カード本体:半透明ガラス調 */
.stagger-card {
  width: min(340px, 86vw);
  padding: 22px 24px 18px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(6px);
}
.stagger-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.stagger-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: #6df0c2; box-shadow: 0 0 12px #6df0c2;
}
.stagger-title { margin: 0; font-size: 17px; letter-spacing: .02em; font-weight: 700; }

.stagger-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

/* 各行:初期は不可視+少し下&右、JSで .is-in を付与して表示 */
.stagger-item {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 13px;
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.05);
  font-size: 14px;
  opacity: 0;
  transform: translate3d(14px, 8px, 0);
  transition: opacity .5s ease, transform .5s cubic-bezier(.22, 1, .36, 1);
}
.stagger-item.is-in { opacity: 1; transform: translate3d(0, 0, 0); }
.stagger-ico { color: #7c8bff; font-size: 11px; }

.stagger-replay {
  margin-top: 16px;
  width: 100%;
  padding: 9px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 10px;
  background: rgba(124, 139, 255, 0.18);
  color: #dfe3ff;
  font-size: 13px;
  cursor: pointer;
  transition: background .2s ease, transform .1s ease;
}
.stagger-replay:hover { background: rgba(124, 139, 255, 0.32); }
.stagger-replay:active { transform: scale(.98); }

/* モーション控えめ設定なら即時表示 */
@media (prefers-reduced-motion: reduce) {
  .stagger-item { transition: none; opacity: 1; transform: none; }
}
JavaScript
// staggered 出現:各 li に時間差で is-in を付与する
(() => {
  const list = document.getElementById('staggerList');
  const replay = document.getElementById('staggerReplay');
  if (!list) return; // null安全

  const items = Array.from(list.querySelectorAll('.stagger-item'));
  const STEP = 90; // 1行ごとの遅延(ms)

  // 一度全消ししてから時間差で再表示
  const play = () => {
    items.forEach((el) => el.classList.remove('is-in'));
    items.forEach((el, i) => {
      setTimeout(() => el.classList.add('is-in'), 120 + i * STEP);
    });
  };

  // 初回再生(reduced-motion でも is-in は付くが遷移は無効)
  play();
  if (replay) replay.addEventListener('click', play);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「staggered リスト出現」の効果を追加してください。

# 追加してほしい効果
staggered リスト出現(アニメーション & トランジション)
リスト各行を時間差でフェード&スライドインさせる演出。タスク一覧や検索結果の登場アニメーションに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- staggered リスト出現デモ:各行が時間差でフェード&スライドイン -->
<div class="stagger-stage">
  <div class="stagger-card">
    <header class="stagger-head">
      <span class="stagger-dot"></span>
      <h2 class="stagger-title">Today's Tasks</h2>
    </header>
    <ul class="stagger-list" id="staggerList">
      <li class="stagger-item"><span class="stagger-ico">◆</span> 朝のストレッチ</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> メールを確認</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> デザインレビュー</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> ランチ休憩</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> プロト実装</li>
      <li class="stagger-item"><span class="stagger-ico">◆</span> 振り返りメモ</li>
    </ul>
    <button class="stagger-replay" id="staggerReplay" type="button">▶ もう一度再生</button>
  </div>
</div>

【CSS】
/* 全体の下地:落ち着いたグラデ背景で中央寄せ */
* { 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: radial-gradient(120% 120% at 20% 0%, #2b2d6e 0%, #14152e 55%, #0b0b1a 100%);
  color: #f4f5ff;
}
.stagger-stage { padding: 24px; width: 100%; display: grid; place-items: center; }

/* カード本体:半透明ガラス調 */
.stagger-card {
  width: min(340px, 86vw);
  padding: 22px 24px 18px;
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(6px);
}
.stagger-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.stagger-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: #6df0c2; box-shadow: 0 0 12px #6df0c2;
}
.stagger-title { margin: 0; font-size: 17px; letter-spacing: .02em; font-weight: 700; }

.stagger-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

/* 各行:初期は不可視+少し下&右、JSで .is-in を付与して表示 */
.stagger-item {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 13px;
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.05);
  font-size: 14px;
  opacity: 0;
  transform: translate3d(14px, 8px, 0);
  transition: opacity .5s ease, transform .5s cubic-bezier(.22, 1, .36, 1);
}
.stagger-item.is-in { opacity: 1; transform: translate3d(0, 0, 0); }
.stagger-ico { color: #7c8bff; font-size: 11px; }

.stagger-replay {
  margin-top: 16px;
  width: 100%;
  padding: 9px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 10px;
  background: rgba(124, 139, 255, 0.18);
  color: #dfe3ff;
  font-size: 13px;
  cursor: pointer;
  transition: background .2s ease, transform .1s ease;
}
.stagger-replay:hover { background: rgba(124, 139, 255, 0.32); }
.stagger-replay:active { transform: scale(.98); }

/* モーション控えめ設定なら即時表示 */
@media (prefers-reduced-motion: reduce) {
  .stagger-item { transition: none; opacity: 1; transform: none; }
}

【JavaScript】
// staggered 出現:各 li に時間差で is-in を付与する
(() => {
  const list = document.getElementById('staggerList');
  const replay = document.getElementById('staggerReplay');
  if (!list) return; // null安全

  const items = Array.from(list.querySelectorAll('.stagger-item'));
  const STEP = 90; // 1行ごとの遅延(ms)

  // 一度全消ししてから時間差で再表示
  const play = () => {
    items.forEach((el) => el.classList.remove('is-in'));
    items.forEach((el, i) => {
      setTimeout(() => el.classList.add('is-in'), 120 + i * STEP);
    });
  };

  // 初回再生(reduced-motion でも is-in は付くが遷移は無効)
  play();
  if (replay) replay.addEventListener('click', play);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。