Elements fade up into place as they enter the viewport. Subtle motion, polished feel, no fancy library needed.
Scroll inside the frame. Each card fades in as it enters view.
This fades up when it enters the viewport.
Each card gets its own observation, so they pop in independently.
Once visible, they stay visible — no re-triggering on scroll-back.
This is the classic IntersectionObserver pattern, ~12 lines of JS.
Reduced-motion users see the cards already in place, no animation.
.reveal {
opacity: 0;
transform: translate3d(0, 24px, 0);
transition:
opacity 900ms cubic-bezier(0.19, 1, 0.22, 1),
transform 900ms cubic-bezier(0.19, 1, 0.22, 1);
will-change: transform, opacity;
}
.reveal.is-visible {
opacity: 1;
transform: translate3d(0, 0, 0);
}
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; }
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); /* stop watching once revealed */
}
});
}, { threshold: 0.15, rootMargin: '0px 0px -50px 0px' });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
Stagger delay — add style="transition-delay: 100ms" to siblings to make them cascade in sequence rather than simultaneously.
From the side — change translateY(24px) to translateX(-24px) for slide-from-left, or right. Use sparingly — varied directions feel chaotic.
Threshold tuning — bump threshold to 0.3 if you want elements to reveal only when ~30% visible (waits longer, feels more deliberate). Lower for snappier feel.
Source: Chevy 55 landing page (user-uploaded HTML, 2026-05-18).