スタッガーアニメーション

sibling-index / sibling-count を使ったSample


概要

- `sibling-index()` 関数と `sibling-count()` 関数は、兄弟要素の「並び順」や「総数」を整数として返すCSS関数 - 数値として使えるので、 `calc()` 関数などと組み合わせて利用可能 - どちらの関数も、実際に返される数値をブラウザの開発ツール上で確認可能 - `sibling-index()` 関数 - 指定した要素が「兄弟要素のなかで何番目か」を整数で返す - 番号のカウントは、 `nth-child()` 疑似クラスと同じく1から始まる - `sibling-count()` 関数 - 「兄弟要素の総数」を整数で返す - 要素自身もカウントに含む(兄弟要素+自分)

デモ

少し回転しながら順番にぱらぱらと表示されるアニメーション (スタッガーアニメーション) の例。
ボタンを押して枠に is_show のクラス名が付与されると、スタッガーアニメーションで表示される形式。
また、スクロールして要素が画面に表示された際にも、 Intersection Observerを使い、自動でクラス名が付与されて表示される仕組みを併用。

  • Card A
  • Card B
  • Card C
  • Card D
  • Card E
  • Card F

コード

HTML

<div class="wrap">
  <button id="toggle_button" type="button">表示する</button>

  <ul id="list" class="cards" aria-live="polite">
    <li>Card A</li>
    <li>Card B</li>
    <li>Card C</li>
    <li>Card D</li>
    <li>Card E</li>
    <li>Card F</li>
  </ul>
</div>

CSS

ul.cards > li {
  --stagger: 70ms;
  --duration: 460ms;
  --ease: cubic-bezier(.2,.8,.2,1);
  --rotate-deg: calc((sibling-index() - 1) * 1.5deg);

  /* “消えている状態”をデフォルトにしておく */
  opacity: 0;
  transform: translateY(14px) scale(.98) rotate(var(--rotate-deg));
  filter: blur(2px);

  transition-property: opacity, transform, filter, rotate;
  transition-duration: var(--duration);
  transition-timing-function: var(--ease);

  /* 非表示へ戻る(=クラスが外れる)時は “逆順” に遅延させたいので sibling-count() を使う */
  transition-delay: calc((sibling-count() - sibling-index()) * var(--stagger));
}

/* 表示時:上から順に遅延(0始まりにしたいので -1) */
ul.cards.is_show > li {
  opacity: 1;
  transform: none;
  filter: none;
  transition-delay: calc((sibling-index() - 1) * var(--stagger));
}

/* ユーザーがユーザーが余計な動きを最少化するOSの設定を有効化している場合は、アニメーションを無効化 */
@media (prefers-reduced-motion: reduce){
  ul.cards > li { transition: none; }
}

/* 以下、表示調整用 */

.wrap {
  margin: 0 auto;
  padding: 16px;
  border-radius: 5px;
  background: #ddd;
}

button {
  padding: 10px 14px;
  border-radius: 10px;
  border: 1px solid #ddd;
  background: #fff;
  cursor: pointer;
}

ul.cards {
  list-style: none;
  padding: 0;
  margin: 18px 0 0;
  display: grid;
  gap: 12px;
}

ul.cards > li {
  margin: 0;
  padding: 14px 16px;
  border: 1px solid #e6e6e6;
  border-radius: 14px;
  background: #fafafa;
}

JavaScript

/**
  * 表示/非表示切り替えボタンの初期化
  */
function setupToggleButton() {
  const btn = document.getElementById('toggle_button');
  const list = document.getElementById('list');

  btn.addEventListener('click', () => {
    const show = list.classList.toggle('is_show');
    btn.textContent = show ? '隠す' : '表示する';
  });
}

/**
  * スクロールして表示されたら表示するクラスを付与する Intersection Observer の初期化
  */
function setupIntersectionObserver() {
  const targets = document.querySelectorAll('.cards');

  const io = new IntersectionObserver((entries, observer) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) {
        // 画面に表示されていない状態
        continue;
      }

      // 表示されたらクラスを付与
      entry.target.classList.add('is_show');

      // ボタンの文言も変更
      const btn = document.getElementById('toggle_button');
      btn.textContent = '隠す';

      // 一度表示したら二度と非表示に戻さない(監視解除)
      observer.unobserve(entry.target);
    }
  }, {
    root: null,
    threshold: 0.15, // 15%が見えたら発火
    rootMargin: '0px 0px -10% 0px' // 少し早め/遅め調整したい時に便利
  });

  targets.forEach(el => io.observe(el));
}

window.addEventListener('DOMContentLoaded', () => {
  setupToggleButton();
  setupIntersectionObserver();
});

参照