少し回転しながら順番にぱらぱらと表示されるアニメーション (スタッガーアニメーション) の例。
ボタンを押して枠に is_show のクラス名が付与されると、スタッガーアニメーションで表示される形式。
また、スクロールして要素が画面に表示された際にも、 Intersection Observerを使い、自動でクラス名が付与されて表示される仕組みを併用。
<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>
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;
}
/**
* 表示/非表示切り替えボタンの初期化
*/
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();
});