<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/the-intersection-observer-api-epzj0j34t" -->

---
title: The Intersection Observer API | daily.dev
description: A comprehensive tutorial covering the Intersection Observer API from first principles: why scroll listeners plus getBoundingClientRect cause jank, how observer...
canonical: https://daily.dev/posts/the-intersection-observer-api-epzj0j34t
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: The Intersection Observer API | daily.dev
og:description: A comprehensive tutorial covering the Intersection Observer API from first principles: why scroll listeners plus getBoundingClientRect cause jank, how observer...
og:url: https://daily.dev/posts/the-intersection-observer-api-epzj0j34t
og:image: https://api.daily.dev/og/posts/epzJ0J34t.png
og:image:alt: The Intersection Observer API
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# The Intersection Observer API

**[Flavio Copes](https://daily.dev/sources/flaviocopes)** · 10 min read · 7 upvotes · 0 comments

## Summary

A comprehensive tutorial covering the Intersection Observer API from first principles: why scroll listeners plus getBoundingClientRect cause jank, how observer entries and options (root, rootMargin, threshold) work, and hands-on implementations of lazy loading images, infinite scroll, scroll-triggered animations, scrollspy navigation, video autoplay pausing, and visibility/impression tracking. Closes with a list of common pitfalls: the initial synchronous callback firing, threshold 1 never firing on elements taller than the viewport, rootMargin requiring units, hidden elements not intersecting, and the API's non-instant batched timing. Also notes native loading="lazy" as an alternative for simple image cases and mentions the Chromium-only Intersection Observer v2 trackVisibility proposal.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://flaviocopes.com/intersection-observer>

## Questions this post answers

### Why doesn't my IntersectionObserver callback fire when threshold is set to 1 on a tall element?

An element taller than the viewport can never be fully visible at once, so its intersection ratio never reaches 1 and the callback never fires. This commonly breaks triggers on mobile screens. The fix is to use a lower threshold value, or use threshold: 0 combined with a negative rootMargin instead.

_Debugging visibility triggers gets easier when API gotchas like this surface right in your daily.dev feed._

### Why does my IntersectionObserver callback fire immediately when I call observe()?

Calling observe() always triggers the callback once immediately to report the element's current intersection state, even before any scrolling happens. If the element starts outside the viewport, that first entry reports isIntersecting: false. Code should check isIntersecting explicitly rather than assuming every callback invocation means the user just scrolled.

_Understanding API quirks like this saves debugging time when daily.dev readers build scroll-based features._

### Should I use the loading="lazy" HTML attribute or the Intersection Observer API for lazy loading images?

Use the native loading="lazy" attribute for plain image lazy loading, since browsers now handle that case natively. Reach for Intersection Observer instead when more control is needed, such as loading a component, swapping a video poster, or starting an expensive render before the element is visible.

_Choosing the right lazy-loading approach matters, daily.dev surfaces these performance trade-offs for frontend developers._

## Similar posts on daily.dev

- [How to track user behavior with JavaScript \(without bloating your site\)](https://daily.dev/posts/how-to-track-user-behavior-with-javascript-without-bloating-your-site--eqwslog0x) · SitePoint · 2 upvotes · 0 comments
- [Creating Scroll-Based Animations in Full view\(\)](https://daily.dev/posts/creating-scroll-based-animations-in-full-view--mo1gx2c3g) · CSS-Tricks · 4 upvotes · 0 comments
- [Scroll-driven CSS animations](https://daily.dev/posts/scroll-driven-css-animations-uw6plcluy) · Flavio Copes · 142 upvotes · 6 comments
- [Virtual Scroll-Driven 3D Scenes – Frontend Masters Blog](https://daily.dev/posts/virtual-scroll-driven-3d-scenes-frontend-masters-blog-ssxkgzbrm) · Frontend Masters · 2 upvotes · 0 comments

---

Tags: [#webdev](https://daily.dev/tags/webdev), [#javascript](https://daily.dev/tags/javascript), [#dom](https://daily.dev/tags/dom)

[View this post on daily.dev](https://daily.dev/posts/the-intersection-observer-api-epzj0j34t)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"The Intersection Observer API","url":"https://daily.dev/posts/the-intersection-observer-api-epzj0j34t","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/the-intersection-observer-api-epzj0j34t"},"datePublished":"2026-08-16T05:53:51.030Z","dateModified":"2026-09-14T06:55:34.459Z","description":"A comprehensive tutorial covering the Intersection Observer API from first principles: why scroll listeners plus getBoundingClientRect cause jank, how observer...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/7219243bb6b6113ecc90bd933965a4ef?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/7219243bb6b6113ecc90bd933965a4ef?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Flavio Copes","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Flavio Copes","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/1f2dbd275797411bbd37bfdaaf0951a9","url":"https://daily.dev/sources/flaviocopes"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/the-intersection-observer-api-epzj0j34t","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":7},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"webdev,javascript,dom","timeRequired":"PT10M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Flavio Copes","item":"https://daily.dev/sources/flaviocopes"},{"@type":"ListItem","position":3,"name":"The Intersection Observer API"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/the-intersection-observer-api-epzj0j34t#faq","mainEntity":[{"@type":"Question","name":"Why doesn't my IntersectionObserver callback fire when threshold is set to 1 on a tall element?","acceptedAnswer":{"@type":"Answer","text":"An element taller than the viewport can never be fully visible at once, so its intersection ratio never reaches 1 and the callback never fires. This commonly breaks triggers on mobile screens. The fix is to use a lower threshold value, or use threshold: 0 combined with a negative rootMargin instead. Debugging visibility triggers gets easier when API gotchas like this surface right in your daily.dev feed."}},{"@type":"Question","name":"Why does my IntersectionObserver callback fire immediately when I call observe()?","acceptedAnswer":{"@type":"Answer","text":"Calling observe() always triggers the callback once immediately to report the element's current intersection state, even before any scrolling happens. If the element starts outside the viewport, that first entry reports isIntersecting: false. Code should check isIntersecting explicitly rather than assuming every callback invocation means the user just scrolled. Understanding API quirks like this saves debugging time when daily.dev readers build scroll-based features."}},{"@type":"Question","name":"Should I use the loading=\"lazy\" HTML attribute or the Intersection Observer API for lazy loading images?","acceptedAnswer":{"@type":"Answer","text":"Use the native loading=\"lazy\" attribute for plain image lazy loading, since browsers now handle that case natively. Reach for Intersection Observer instead when more control is needed, such as loading a component, swapping a video poster, or starting an expensive render before the element is visible. Choosing the right lazy-loading approach matters, daily.dev surfaces these performance trade-offs for frontend developers."}}]}
```

