Curtain image revealWeb · App
Target
Implement the "Staggered reveal on enter" effect in vanilla HTML/CSS/JS. Context: Children fade and rise in sequence the first time their container scrolls into view. Trigger: the element enters the viewport (first time only). Timeline: time-based (duration + easing). Motion tokens (normal): - opacity: 0 → 1 - translateY: 16px → 0 - duration: 600ms - easing: cubic-bezier(0.2, 0.8, 0.2, 1) - stagger: 80ms (cap at 6 items) Motion tokens (prefers-reduced-motion): - opacity: 0 → 1 - duration: 200ms - easing: ease-out Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - plays once per element; replays only on explicit replay - items beyond the stagger cap animate together with the last delayed item
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-staggered-reveal-on-enter" data-fx-root>
<div class="fx-list">
<div class="fx-item fx-title"></div>
<div class="fx-item"></div>
<div class="fx-item fx-w85"></div>
<div class="fx-item fx-w45"></div>
</div>
</div>
demo.css
.fx-staggered-reveal-on-enter { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-staggered-reveal-on-enter .fx-list { display: flex; flex-direction: column; gap: 10px; width: 62%; }
.fx-staggered-reveal-on-enter .fx-item { height: 12px; border-radius: 5px; background: var(--fx-muted, #e4e4e7); opacity: 0; transform: translateY(16px); }
.fx-staggered-reveal-on-enter .fx-title { height: 18px; width: 55%; background: var(--fx-strong, #d4d4d8); }
.fx-staggered-reveal-on-enter .fx-w85 { width: 85%; }
.fx-staggered-reveal-on-enter .fx-w45 { width: 45%; }
.fx-staggered-reveal-on-enter.is-playing .fx-item {
animation: fx-sr-rise 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
animation-delay: calc(var(--i, 0) * 80ms);
}
@keyframes fx-sr-rise { to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
.fx-staggered-reveal-on-enter .fx-item { transform: none; }
.fx-staggered-reveal-on-enter.is-playing .fx-item { animation: fx-sr-fade 200ms ease-out both; animation-delay: 0ms; }
@keyframes fx-sr-fade { to { opacity: 1; } }
}
demo.js
// Contract: export default mount(root) → { play, stop }
export default function mount(root) {
const items = root.querySelectorAll('.fx-item');
items.forEach((el, i) => el.style.setProperty('--i', String(Math.min(i, 5))));
return {
play() { root.classList.remove('is-playing'); void root.offsetWidth; root.classList.add('is-playing'); },
stop() { root.classList.remove('is-playing'); },
};
}
Inspired by m3.material.io — Material 3 staggered list guidance. The preview is an original re-creation; the effect name belongs to its author.
Pure CSS keyframes; JS only toggles `.is-playing` and sets `--i` per child (capped at 5 so long lists do not wait seconds). Production tip: trigger with IntersectionObserver `threshold: 0.25`, `unobserve` after first play.
Added 2026-11-09 · Report an issue