Frontend Master

JavaScript Curriculum

Intersection Observer
+60 XP

Intersection Observer

medium
~30 min·60 XP

The coffee shop menu has 50 item images that all load at once, slowing the page to a crawl. The specials section should animate in as users scroll down. Intersection Observer handles both — efficiently, with zero scroll listener overhead.

Intersection Observer

scroll event listeners fire hundreds of times per second — expensive and janky. The IntersectionObserver API is asynchronous and browser-optimized: it tells you when elements enter or leave the viewport without blocking the main thread.

How IntersectionObserver Works

Watch elements scroll into view — adjust the threshold to see how it changes when callbacks fire:

intersection-observer.js
threshold:0.5= 50% visible
Card #1○ hidden
Card #2○ hidden
Card #3○ hidden
Card #4○ hidden
Card #5○ hidden
Card #6○ hidden
Callback log
// Scroll the cards below
const io = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      entry.isIntersecting
        ? el.classList.add('visible')
        : el.classList.remove('visible')
    })
  },
  { threshold: 0.5 }
)
io.observe(el)
ℹ️threshold vs rootMargin
`threshold: 0.5` fires when 50% of the element is visible. `rootMargin: '100px'` extends the detection zone — fires 100px before the element reaches the viewport edge. Use rootMargin for preloading (load before visible) and threshold for animations (trigger when partially visible).

Lazy Loading Images

Load images only when they approach the viewport — classic IntersectionObserver use case:

lazy-load.js0/6 loaded
lazy
lazy
lazy
lazy
lazy
lazy
// Scroll to lazy-load images
// rootMargin: '50px' loads
// slightly before visible
const io = new IntersectionObserver(
  (entries) => entries.forEach(e => {
    if (!e.isIntersecting) return
    loadImage(e.target)
    io.unobserve(e.target) // once!
  }),
  { rootMargin: '50px', threshold: 0 }
)
imgs.forEach(img => io.observe(img))
💡unobserve after loading
Always call `io.unobserve(entry.target)` inside the callback once you have loaded the image or triggered the animation. Keeps watching an element that is already loaded wastes memory. Only call `io.disconnect()` when you want to stop observing everything.

Scroll-Triggered Animations

Animate elements in as they scroll into view — one observer handles all cards:

scroll-animation.js
Espresso
Cappuccino
Pour Over
Cold Brew
Flat White
Matcha Latte
CSS
.card {
  opacity:0;
  transform:translateY(30px)
  transition: all 500ms ease;
}
.card.visible {
  opacity:1;
  transform:translateY(0)
}
const io = new IntersectionObserver(
  entries => entries.forEach(e => {
    if (e.isIntersecting)
      e.target.classList.add('visible')
  }),
  { threshold: 0.2 }
)
cards.forEach(c => io.observe(c))
⚠️Respect prefers-reduced-motion
Always wrap scroll animations with `@media (prefers-reduced-motion: reduce)` in CSS — disable or simplify animations for users who have opted out. Some users experience motion sickness from animations.

Your Challenge

Select all .card elements and observe each one. On intersection (threshold: 0.2), add .visible class and unobserve. Add CSS for the initial hidden state and the .visible animated state. Use rootMargin: '0px 0px -50px 0px' to trigger animations slightly before the card fully enters the viewport.

Challenge

Observe all .card elements. When a card enters the viewport (threshold: 0.2), add a 'visible' class and unobserve it. Add CSS: .card { opacity: 0; transform: translateY(20px); transition: all 400ms; } .card.visible { opacity: 1; transform: none; }

IntersectionObserverobserveunobservedisconnectisIntersectingintersectionRatiothresholdrootMarginlazy-loadingscroll-animation
Intersection Observer | Nexus Learn