const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue
entry.target.classList.add('is-visible')
observer.unobserve(entry.target)
}
},
{ rootMargin: '0px 0px -10% 0px', threshold: 0.1 }
)
document.querySelectorAll('.card').forEach((card) => observer.observe(card))
threshold: 0.1 fires when a tenth of the element shows; the negative bottom rootMargin waits until it is a bit inside the viewport instead of right at the edge. Unobserving after the first time keeps it from running on every scroll.
Scroll listeners run on every pixel of scrolling; the observer only calls you when something crosses the line, which is why it is the right tool for this.