├── .pr-preview.json ├── w3c.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── compile.sh ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── auto-publish.yml ├── Makefile ├── README.md ├── CONTRIBUTING.md ├── explainer.md └── index.bs /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.bs", 3 | "type": "bikeshed", 4 | "params": { 5 | "force": 1 6 | } 7 | } -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": [ 3 | "32061" 4 | ], 5 | "contacts": [ 6 | "svgeesus" 7 | ], 8 | "shortName": "IntersectionObserver", 9 | "repo-type": "rec-track" 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors under the 2 | [W3C Software and Document 3 | License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). Contributions to 4 | Specifications are made under the [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 5 | 6 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e # Exit with nonzero exit code if anything fails 3 | 4 | bikeshed spec 5 | 6 | outdir=out 7 | if [ -n "$1" ]; then 8 | outdir="$1" 9 | fi 10 | 11 | if [ -d "${outdir}" ]; then 12 | echo Copy the generated spec into "${outdir}/index.html" 13 | cp index.html "${outdir}/index.html" 14 | fi 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes #??? 2 | 3 | The following tasks have been completed: 4 | 5 | * [ ] Modified Web platform tests (link to pull request) 6 | 7 | Implementation commitment: 8 | 9 | * [ ] WebKit (https://bugs.webkit.org/show_bug.cgi?id=) 10 | * [ ] Chromium (https://bugs.chromium.org/p/chromium/issues/detail?id=) 11 | * [ ] Gecko (https://bugzilla.mozilla.org/show_bug.cgi?id=) 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -o pipefail 2 | .PHONY: local remote deploy 3 | 4 | remote: index.bs 5 | @ (HTTP_STATUS=$$(curl https://api.csswg.org/bikeshed/ \ 6 | --output index.html \ 7 | --write-out "%{http_code}" \ 8 | --header "Accept: text/plain, text/html" \ 9 | -F die-on=warning \ 10 | -F md-Text-Macro="COMMIT-SHA LOCAL COPY" \ 11 | -F file=@index.bs) && \ 12 | [[ "$$HTTP_STATUS" -eq "200" ]]) || ( \ 13 | echo ""; cat index.html; echo ""; \ 14 | rm -f index.html; \ 15 | exit 22 \ 16 | ); 17 | 18 | local: index.bs 19 | bikeshed spec index.bs index.html --md-Text-Macro="COMMIT-SHA LOCAL COPY" 20 | -------------------------------------------------------------------------------- /.github/workflows/auto-publish.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: [main] 6 | permissions: 7 | contents: read 8 | jobs: 9 | main: 10 | name: Build, Validate, Deploy 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: w3c/spec-prod@v2 17 | with: 18 | GH_PAGES_BRANCH: gh-pages 19 | TOOLCHAIN: bikeshed 20 | BUILD_FAIL_ON: "link-error" 21 | W3C_NOTIFICATIONS_CC: ${{ secrets.CC }} 22 | W3C_ECHIDNA_TOKEN: ${{ secrets.ECHIDNA_TOKEN }} 23 | W3C_WG_DECISION_URL: https://lists.w3.org/Archives/Public/public-webapps/2014JulSep/0627.html 24 | W3C_BUILD_OVERRIDE: | 25 | status: WD 26 | shortname: intersection-observer 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intersection Observers 2 | 3 | Implementation status: 4 | - Chromium: [Shipped in Chrome 51](https://www.chromestatus.com/feature/5695342691483648) 5 | - Edge: [Shipped in build 14986](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/intersectionobserver/) 6 | - Firefox: [Shipped in Firefox 55](https://platform-status.mozilla.org/#intersection-observer) 7 | - WebKit: [Shipped in Safari 12.1 and iOS 12.2](https://developer.apple.com/documentation/safari_release_notes/safari_12_1_release_notes#3130314) 8 | 9 | [Explainer Doc](./explainer.md) 10 | 11 | [Draft Spec](https://w3c.github.io/IntersectionObserver/) 12 | 13 | Given that multiple native implementations of IntersectionObserver exist today, the polyfill that 14 | was formerly maintained in this repo is no longer supported and has been archived at https://github.com/GoogleChromeLabs/intersection-observer. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Working Group 2 | 3 | Contributions to this repository are intended to become part of Recommendation-track documents 4 | governed by the [W3C Patent Policy](http://www.w3.org/Consortium/Patent-Policy-20040205/) and 5 | [Document License](http://www.w3.org/Consortium/Legal/copyright-documents). To contribute, you must 6 | either participate in the relevant W3C Working Group or make a non-member patent licensing 7 | commitment. 8 | 9 | If you are not the sole contributor to a contribution (pull request), please identify all 10 | contributors in the pull request's body or in subsequent comments. 11 | 12 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 13 | 14 | ``` 15 | +@github_username 16 | ``` 17 | 18 | If you added a contributor by mistake, you can remove them in a comment with: 19 | 20 | ``` 21 | -@github_username 22 | ``` 23 | 24 | If you are making a pull request on behalf of someone else but you had no part in designing the 25 | feature, you can remove yourself with the above syntax. 26 | 27 | # Tests 28 | 29 | For normative changes, a corresponding 30 | [web-platform-tests](https://github.com/web-platform-tests/wpt) PR is highly appreciated. Typically, 31 | both PRs will be merged at the same time. Note that a test change that contradicts the spec should 32 | not be merged before the corresponding spec change. If testing is not practical, please explain why 33 | and if appropriate [file an issue](https://github.com/web-platform-tests/wpt/issues/new) to follow 34 | up later. Add the `type:untestable` or `type:missing-coverage` label as appropriate. 35 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # Intersection Observers Explained 2 | 3 | ## What's All This About? 4 | 5 | This repo outlines an API that can be used to understand movement of DOM elements relative to another element or the browser top level viewport. Changes are delivered asynchronously and are useful for understanding the visibility of elements, managing pre-loading of DOM and data, as well as deferred loading of "below the fold" page content. 6 | 7 | ## Observing Position 8 | 9 | The web's traditional position calculation mechanisms rely on explicit queries of DOM state. Some of these are known to cause style recalculation and layout and, frequently, are redundant thanks to the requirement that scripts poll for this information. 10 | 11 | A body of common practice has evolved that relies on these behaviors, however, including (but not limited to): 12 | 13 | * Observing the location of "below the fold" sections of content in order to lazy-load content. 14 | * Implementing data-bound high-performance scrolling lists which load and render subsets of data sets. These lists are a central mobile interaction idiom. 15 | * Calculating element visibility. In particular, [ad networks now require reporting of ad "visibility" for monetizing impressions](https://www.iab.com/news/viewability-has-arrived-what-you-need-to-know-to-see-through-this-sea-change/). This has led to many sites abusing scroll handlers, [synchronous layout invoking readbacks](https://gist.github.com/paulirish/5d52fb081b3570c81e3a), and resorting to exotic plugin-based solutions for computing "true" element visibility (as a fraction of the element's intended size). 16 | 17 | These use-cases have several common properties: 18 | 19 | 1. They can be represented as passive "queries" about the state of individual elements with respect to some other element (or the global viewport). 20 | 1. They do not impose hard latency requirements; that is to say, the information can be delayed somewhat asynchronously (e.g. from another thread) without penalty. 21 | 1. They are poorly supported by nearly all combinations of existing web platform features, requiring extraordinary developer effort despite their widespread use. 22 | 23 | A notable non-goal is pixel-accurate information about what was actually displayed (which can be quite difficult to obtain efficiently in certain browser architectures in the face of filters, webgl, and other features). In all of these scenarios the information is useful even when delivered at a slight delay and without perfect compositing-result data. 24 | 25 | Given the opportunity to reduce CPU use, increase battery life, and eliminate jank it seems like a new API to simplify answering these queries is a prudent addition to the web platform. 26 | 27 | ### Proposed API 28 | 29 | ```js 30 | function callback(entries) { 31 | entries.forEach(function(entry) { 32 | if (entry.isIntersecting) 33 | doSomething(); 34 | }); 35 | }; 36 | var root = document.getElementById('root'); 37 | var target = document.getElementById('target'); 38 | var options_dict = { 39 | thresholds: [0.0, 0.3, 0.7, 1.0], 40 | root: root 41 | }; 42 | var observer = new IntersectionObserver(callback, options_dict); 43 | observer.observe(target); 44 | ``` 45 | 46 | The expected use of this API is that you create an IntersectionObserver with a root element; then observe one or more elements that are descendants of the root. The callback will be fired whenever any of the observed elements' ratio of (area of observed element's intersection with root / total area of observed element) crosses any of the observer's thresholds (i.e., transitions from less than the threshold to greater, or vice versa). The callback includes change records for all observed elements for which a threshold crossing has occurred since the last callback. 47 | 48 | ## Element Visibility 49 | 50 | The information provided by this API, combined with the default viewport query, allows a developer to easily understand when an element comes into, or passes out of, view. Here's how one might implement the IAB's "50% visible for more than a continuous second" policy for counting an ad impression: 51 | 52 | ```html 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | ```js 60 | // ads.js 61 | 62 | function logImpressionToServer() { /* ... */ } 63 | 64 | function isVisible(boundingClientRect, intersectionRect) { 65 | return ((intersectionRect.width * intersectionRect.height) / 66 | (boundingClientRect.width * boundingClientRect.height) >= 0.5); 67 | } 68 | 69 | function visibleTimerCallback(element, observer) { 70 | delete element.visibleTimeout; 71 | // Process any pending observations 72 | processChanges(observer.takeRecords()); 73 | if ('isVisible' in element) { 74 | delete element.isVisible; 75 | logImpressionToServer(); 76 | observer.unobserve(element); 77 | } 78 | } 79 | 80 | function processChanges(changes) { 81 | changes.forEach(function(changeRecord) { 82 | var element = changeRecord.target; 83 | element.isVisible = isVisible(changeRecord.boundingClientRect, changeRecord.intersectionRect); 84 | if ('isVisible' in element) { 85 | // Transitioned from hidden to visible 86 | element.visibleTimeout = setTimeout(visibleTimerCallback, 1000, element, observer); 87 | } else { 88 | // Transitioned from visible to hidden 89 | if ('visibleTimeout' in element) { 90 | clearTimeout(element.visibleTimeout); 91 | delete element.visibleTimeout; 92 | } 93 | } 94 | }); 95 | } 96 | 97 | var observer = new IntersectionObserver( 98 | processChanges, 99 | { threshold: [0.5] } 100 | ); 101 | 102 | var theAd = document.querySelector('#theAd'); 103 | observer.observe(theAd); 104 | ``` 105 | 106 | If more granular information about visibility is needed, the above code may be modified to use a sequence of threshold values. This higher rate of delivery might seem expensive at first glance, but note the power and performance advantages over current practice: 107 | 108 | - No scroll handlers need be installed/run (a frequent source of jank). 109 | - Off-screen ads do not deliver any events or set any timers until they come into view. 110 | - No polling, synchronous layouts, or plugins are required; only a single timeout to record the completed ad impression. 111 | 112 | ## Data Scrollers 113 | 114 | Many systems use data-bound lists which manage their in-view contents, recycling DOM to remain memory and layout-efficient while triggering loading of data that will be needed at some point in the near future. 115 | 116 | These systems frequently want to use different queries on the same scroll-containing viewport. Data loading can take a long time, so it is advantageous to pre-populate data stores with significantly more information than is visible. The rendered element count may display a much smaller subset of available data; only the "skirt" on each side of a scrolling area necessary to keep up with scrolling velocity (to avoid "blank" or "checkerboard" data). 117 | 118 | We can use an `IntersectionObserver` on child elements of a parent scrolling element to inform the system when to load data and recycle scrolled-out-of-view elements and stamp new content into them for rendering at the "end" of the list: 119 | 120 | ```html 121 | 145 | 146 |
147 |
148 |
item 1
149 |
item 2
150 |
item 3
151 | 152 |
153 |
154 | ``` 155 | 156 | As the user moves the `container`, the children can be observed and as they cross the threshold of the scrollable area, a manager can recycle them and fill them with new data instead of needing to re-create the items from scratch. 157 | 158 | ```js 159 | function query(selector) { 160 | return Array.from(document.querySelectorAll(selector)); 161 | } 162 | 163 | function init() { 164 | // Notify when a scroll-item gets within, or moves beyond, 500px from the visible scroll surface. 165 | var opts = { 166 | root: document.querySelector(".container"), 167 | rootMargin: "500px 0px" 168 | }; 169 | var observer = new IntersectionObserver(manageItemPositionChanges, opts); 170 | // Set up observer on the items 171 | query(".inner-scroll-surface > .scroll-item") 172 | .forEach(function(scrollItem) { 173 | observer.observe(scrollItem); 174 | }); 175 | } 176 | 177 | function manageItemPositionChanges(changes) { 178 | // ... 179 | }, 180 | ``` 181 | 182 | Many scrollers also want to fetch even more data than what's displayed in the list. We can create a second observer with a much larger "skirt" outside the viewport which will allow us to fetch a larger data set to account for latency. 183 | 184 | ## Delay Loading 185 | 186 | Many sites like to avoid loading certain resources until they're near the viewport. This is easy to do with an IntersectionObserver: 187 | 188 | ```html 189 | 190 |
191 | 194 |
195 | ``` 196 | 197 | ```js 198 | function query(selector) { 199 | return Array.from(document.querySelectorAll(selector)); 200 | } 201 | 202 | var observer = new IntersectionObserver( 203 | // Pre-load items that are within 2 multiples of the visible viewport height. 204 | function(changes) { 205 | changes.forEach(function(change) { 206 | var container = change.target; 207 | var content = container.querySelector("template").content; 208 | container.appendChild(content); 209 | observer.unobserve(container); 210 | }); 211 | }, 212 | { rootMargin: "200% 0%" } 213 | ); 214 | 215 | // Set up lazy loading 216 | query(".lazy-loaded").forEach(function(item) { 217 | observer.observe(item); 218 | }); 219 | ``` 220 | 221 | ## Open Design Questions 222 | 223 | This is a work in progress! We've tried to pattern the initial design after [`Object.observe()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) and [DOM's Mutation Observers](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver). 224 | 225 | The specific timing of of change record delivery is also TBD. 226 | 227 | Is it meaningful to have overdraw queries against the default viewport? 228 | -------------------------------------------------------------------------------- /index.bs: -------------------------------------------------------------------------------- 1 |
  2 | Title: Intersection Observer
  3 | Status: ED
  4 | ED: https://w3c.github.io/IntersectionObserver/
  5 | Shortname: intersection-observer
  6 | TR: https://www.w3.org/TR/intersection-observer/
  7 | Previous version: from biblio
  8 | Level: none
  9 | Editor: Stefan Zager, Google, szager@google.com, w3cid 91208
 10 | Editor: Emilio Cobos Álvarez , Mozilla, emilio@mozilla.com, w3cid 106537
 11 | Editor: Traian Captan, Google, tcaptan@google.com, w3cid 137959
 12 | Former Editor: Michael Blain, Google, mpb@google.com, w3cid 73819
 13 | Abstract: This specification describes an API that can be used to understand the visibility and position of DOM elements ("targets") relative to a containing element or to the top-level viewport ("root"). The position is delivered asynchronously and is useful for understanding the visibility of elements and implementing pre-loading and deferred loading of DOM content.
 14 | Group: webapps
 15 | Repository: W3C/IntersectionObserver
 16 | Test Suite: http://w3c-test.org/intersection-observer/
 17 | Ignored Vars: rootMargin, docs, now
 18 | Markup Shorthands: markdown yes
 19 | 
20 | 21 |
 22 | urlPrefix: https://dom.spec.whatwg.org/
 23 | 	url: #node-trees; type: dfn; text: node tree
 24 | urlPrefix: http://www.w3.org/TR/hr-time/
 25 | 	type: typedef; text: DOMHighResTimeStamp
 26 | 	type: dfn; text: time origin
 27 | urlPrefix: https://html.spec.whatwg.org/multipage/
 28 | 	urlPrefix: dom.html
 29 | 		url: #document; type:dfn; text: Document
 30 | 	urlPrefix: browsers.html
 31 | 		type: dfn; text: browsing context
 32 | 		type: dfn; text: top-level browsing context
 33 | 		type: dfn; text: nested browsing context
 34 | 		type: dfn; text: list of the descendant browsing contexts
 35 | 		type: dfn; text: browsing context container
 36 | 		type: dfn; text: fully active
 37 | 	urlPrefix: webappapis.html;
 38 | 		type: dfn; text: report the exception
 39 | 		type: dfn; text: event loop
 40 | 		type: dfn; text: queue a task
 41 | 		type: dfn; text: run the fullscreen rendering steps
 42 | 		type: dfn; text: run the animation frame callbacks
 43 | 		url: #event-loop-processing-model; type: dfn; text: HTML Event Loops Processing Model
 44 | 		type: dfn; text: relevant settings object
 45 | 		url: #concept-environment-top-level-origin; type: dfn; text: top-level origin
 46 | 	urlPrefix: infrastructure.html;
 47 | 		url: #dfn-callback-this-value; type: dfn; text: callback this value
 48 | 	urlPrefix: origin.html
 49 | 		type: dfn; text: origin
 50 | urlPrefix: https://heycam.github.io/webidl/
 51 | 	url: #dfn-simple-exception; type:exception;
 52 | 		text: RangeError
 53 | 		text: TypeError
 54 | 		text: SyntaxError
 55 | 	urlPrefix: #dfn-; type:dfn; text: throw
 56 | 	urlPrefix: #idl-; type:interface; text: double
 57 | 	urlPrefix: #idl-; type:interface; text: undefined
 58 | 	url: #hierarchyrequesterror; type: exception; text: HierarchyRequestError
 59 | urlPrefix: https://drafts.csswg.org/css-box/
 60 | 	url: #containing-block; type: dfn; text: containing block
 61 | 	url: #padding-area; type: dfn; text: padding area
 62 | 	url: #padding-edge; type: dfn; text: padding edge
 63 | urlPrefix: https://drafts.csswg.org/css-display/
 64 | 	url: #containing-block-chain; type: dfn; text: containing block chain
 65 | urlPrefix: http://www.w3.org/TR/css-masking-1/
 66 | 	url: #propdef-clip-path; type:dfn; text: clip-path
 67 | urlPrefix: https://drafts.csswg.org/css-overflow-3/
 68 | 	url: #ink-overflow-rectangle; type:dfn; text: ink overflow rectangle
 69 | 	url: #ink-overflow-region; type:dfn; text: ink overflow region
 70 | 	url: #overflow-properties; type:dfn; text: overflow properties
 71 | urlPrefix: https://drafts.csswg.org/css-transforms-1/
 72 | 	url: #transformation-matrix; type:dfn; text: transformation matrix
 73 | 	url: #serialization-of-the-computed-value; type:dfn; text: serialization
 74 | 	url: #identity-transform-function; type:dfn; text: identity transform function
 75 | 	url: #post-multiplied; type:dfn; text: post-multiplied
 76 | urlPrefix: https://drafts.csswg.org/cssom-view-1/
 77 | 	url: #pinch-zoom; type:dfn; text: pinch zoom
 78 | urlPrefix: https://drafts.csswg.org/css2/visuren.html
 79 | 	url: #viewport; type:dfn; text: viewport
 80 | urlPrefix: https://drafts.fxtf.org/filter-effects/
 81 | 	url: #funcdef-filter-blur; type:dfn; text: blur
 82 | 
83 | 84 | 90 | 91 |

Introduction

92 | 93 | The web's traditional position calculation mechanisms 94 | rely on explicit queries of DOM state 95 | that are known to cause (expensive) style recalculation and layout 96 | and, frequently, are a source of significant performance overhead 97 | due to continuous polling for this information. 98 | 99 | A body of common practice has evolved that relies on these behaviors, 100 | however, including (but not limited to): 101 | 102 | * Building custom pre- and deferred-loading of DOM and data. 103 | * Implementing data-bound high-performance scrolling lists 104 | which load and render subsets of data sets. 105 | These lists are a central mobile interaction idiom. 106 | * Calculating element visibility. 107 | In particular, 108 | 109 | ad networks now require reporting of ad "visibility" for monetizing impressions. This has led to many sites abusing scroll handlers 110 | (causing jank on scroll), 111 | 112 | synchronous layout invoking readbacks 113 | (causing unnecessary critical work in rAF loops), 114 | and resorting to exotic plugin-based solutions for computing "true" element visibility (with all the associated overhead of the plugin architecture). 115 | 116 | These use-cases have several common properties: 117 | 118 | 1. They can be represented as passive "queries" 119 | about the state of individual elements 120 | with respect to some other element 121 | (or the global viewport). 122 | 2. They do not impose hard latency requirements; 123 | that is to say, the information can be delivered asynchronously 124 | (e.g. from another thread) 125 | without penalty. 126 | 3. They are poorly supported by nearly all combinations of existing web platform features, 127 | requiring extraordinary developer effort despite their widespread use. 128 | 129 | A notable non-goal is pixel-accurate information about what was actually displayed 130 | (which can be quite difficult to obtain efficiently in certain browser architectures 131 | in the face of filters, webgl, and other features). 132 | In all of these scenarios the information is useful 133 | even when delivered at a slight delay 134 | and without perfect compositing-result data. 135 | 136 | The Intersection Observer API addresses the above issues 137 | by giving developers a new method to asynchronously query the position of an element 138 | with respect to other elements or the global viewport. 139 | The asynchronous delivery eliminates the need for costly DOM and style queries, 140 | continuous polling, 141 | and use of custom plugins. 142 | By removing the need for these methods 143 | it allows applications to significantly reduce their CPU, GPU and energy costs. 144 | 145 |
146 |
147 | 	var observer = new IntersectionObserver(changes => {
148 | 		for (const change of changes) {
149 | 			console.log(change.time);               // Timestamp when the change occurred
150 | 			console.log(change.rootBounds);         // Unclipped area of root
151 | 			console.log(change.boundingClientRect); // target.getBoundingClientRect()
152 | 			console.log(change.intersectionRect);   // boundingClientRect, clipped by its containing block ancestors, and intersected with rootBounds
153 | 			console.log(change.intersectionRatio);  // Ratio of intersectionRect area to boundingClientRect area
154 | 			console.log(change.target);             // the Element target
155 | 		}
156 | 	}, {});
157 | 
158 | 	// Watch for intersection events on a specific target Element.
159 | 	observer.observe(target);
160 | 
161 | 	// Stop watching for intersection events on a specific target Element.
162 | 	observer.unobserve(target);
163 | 
164 | 	// Stop observing threshold events on all target elements.
165 | 	observer.disconnect();
166 | 	
167 |
168 | 169 |

Intersection Observer

170 | 171 | The Intersection Observer API enables developers to understand the 172 | visibility and position of target DOM 173 | elements relative to an intersection root. 174 | 175 |

176 | The IntersectionObserverCallback

177 | 178 |
179 | 	callback IntersectionObserverCallback = undefined (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
180 | 
181 | 182 | This callback will be invoked when there are changes to a target's 183 | intersection with the intersection root, as per the 184 | processing model. 185 | 186 |

187 | The IntersectionObserver interface

188 | 189 | The {{IntersectionObserver}} interface can be used to observe changes in the 190 | intersection of an intersection root and one or more target {{Element}}s. 191 | 192 | The intersection root 193 | for an {{IntersectionObserver}} is the value of its {{IntersectionObserver/root}} attribute 194 | if the attribute is non-null; 195 | otherwise, it is the top-level browsing context's {{document}} node, 196 | referred to as the implicit root. 197 | 198 | An {{IntersectionObserver}} with a non-null {{IntersectionObserver/root}} 199 | is referred to as an explicit root observer, 200 | and it can observe any target {{Element}} that is a descendant of the 201 | {{IntersectionObserver/root}} in the containing block chain. 202 | An {{IntersectionObserver}} with a null {{IntersectionObserver/root}} 203 | is referred to as an implicit root observer. 204 | Valid targets for an implicit root observer include 205 | any {{Element}} in the top-level browsing context, 206 | as well as any {{Element}} in any nested browsing context 207 | which is in the list of the descendant browsing contexts of the top-level browsing context. 208 | 209 | When dealing with implicit root observers, the API makes a distinction between 210 | a target whose relevant settings object's origin is 211 | same origin-domain with the top-level origin, referred to as a 212 | same-origin-domain target; 213 | as opposed to a cross-origin-domain target. 214 | Any target of an explicit root observer is also a same-origin-domain target, 215 | since the target must be in the same document as the 216 | intersection root. 217 | 218 | Note: In {{MutationObserver}}, the {{MutationObserverInit}} options are passed 219 | to {{MutationObserver/observe()}} while in {{IntersectionObserver}} they are 220 | passed to the constructor. This is because for MutationObserver, each {{Node}} 221 | being observed could have a different set of attributes to filter for. For 222 | {{IntersectionObserver}}, developers may choose to use a single observer to 223 | track multiple targets using the same set of options; or they may use a different 224 | observer for each tracked target. 225 | {{IntersectionObserverInit/rootMargin}} or {{threshold}} values for each 226 | target seems to introduce more complexity without solving additional 227 | use-cases. Per-{{observe()}} options could be provided in the future if the need arises. 228 | 229 |
230 | [Exposed=Window]
231 | interface IntersectionObserver {
232 | 	constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options = {});
233 | 	readonly attribute (Element or Document)? root;
234 | 	readonly attribute DOMString rootMargin;
235 | 	readonly attribute DOMString scrollMargin;
236 | 	readonly attribute FrozenArray<double> thresholds;
237 | 	readonly attribute long delay;
238 | 	readonly attribute boolean trackVisibility;
239 | 	undefined observe(Element target);
240 | 	undefined unobserve(Element target);
241 | 	undefined disconnect();
242 | 	sequence<IntersectionObserverEntry> takeRecords();
243 | };
244 | 
245 | 246 |
247 | : 248 | new IntersectionObserver(callback, options) 249 | :: 250 | Return the result of running the initialize a new IntersectionObserver 251 | algorithm, providing |callback| and |options|. 252 | : observe(target) 253 | :: 254 | Run the observe a target Element algorithm, providing |this| and |target|. 255 | : unobserve(target) 256 | :: 257 | Run the unobserve a target Element algorithm, providing |this| and |target|. 258 | 259 | Note: {{MutationObserver}} does not implement {{unobserve()}}. 260 | For {{IntersectionObserver}}, {{unobserve()}} addresses the 261 | lazy-loading use case. After loading is initiated for |target|, 262 | it does not need to be tracked. 263 | It would be more work to either {{disconnect()}} all |target|s 264 | and {{observe()}} the remaining ones, 265 | or create a separate {{IntersectionObserver}} for each |target|. 266 | : disconnect() 267 | :: 268 | For each |target| in |this|'s internal {{[[ObservationTargets]]}} slot: 269 | 270 | 1. Remove the {{IntersectionObserverRegistration}} record 271 | whose {{IntersectionObserverRegistration/observer}} property is equal to |this| 272 | from |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot. 273 | 2. Remove |target| from |this|'s internal {{[[ObservationTargets]]}} slot. 274 | : takeRecords() 275 | :: 276 | 1. Let |queue| be a copy of |this|'s internal {{[[QueuedEntries]]}} slot. 277 | 2. Clear |this|'s internal {{[[QueuedEntries]]}} slot. 278 | 3. Return |queue|. 279 |
280 | 281 |
282 | : root 283 | :: 284 | The {{IntersectionObserverInit/root}} provided to the {{IntersectionObserver}} constructor, or null if none was provided. 285 | : rootMargin 286 | :: 287 | Offsets applied to the root intersection rectangle, 288 | effectively growing or shrinking the box that is used to calculate intersections. 289 | These offsets are only applied when handling same-origin-domain targets; 290 | for cross-origin-domain targets they are ignored. 291 | 292 | On getting, return the result of serializing the elements of {{[[rootMargin]]}} 293 | space-separated, where pixel lengths serialize as the numeric value followed by "px", 294 | and percentages serialize as the numeric value followed by "%". Note that 295 | this is not guaranteed to be identical to the |options|.{{IntersectionObserverInit/rootMargin}} 296 | passed to the {{IntersectionObserver}} constructor. If no 297 | {{IntersectionObserverInit/rootMargin}} was passed to the {{IntersectionObserver}} 298 | constructor, the value of this attribute is "0px 0px 0px 0px". 299 | : scrollMargin 300 | :: 301 | Offsets are applied to scrollports on the path from intersection root to target, 302 | effectively growing or shrinking the clip rects used to calculate intersections. 303 | 304 | On getting, return the result of serializing the elements of {{[[scrollMargin]]}} 305 | space-separated, where pixel lengths serialize as the numeric value followed by "px", 306 | and percentages serialize as the numeric value followed by "%". Note that 307 | this is not guaranteed to be identical to the |options|.{{IntersectionObserverInit/scrollMargin}} 308 | passed to the {{IntersectionObserver}} constructor. If no 309 | {{IntersectionObserverInit/scrollMargin}} was passed to the {{IntersectionObserver}} 310 | constructor, the value of this attribute is "0px 0px 0px 0px". 311 | : thresholds 312 | :: 313 | A list of thresholds, sorted in increasing numeric order, 314 | where each threshold is a ratio of intersection area to bounding box area 315 | of an observed target. Notifications for a target are generated when any 316 | of the thresholds are crossed for that target. 317 | If no |options|.{{IntersectionObserverInit/threshold}} was provided to the 318 | {{IntersectionObserver}} constructor, or the sequence is empty, the value 319 | of this attribute will be [0]. 320 | : delay 321 | :: 322 | A number indicating the minimum delay in milliseconds 323 | between notifications from this observer for a given target. 324 | : trackVisibility 325 | :: 326 | A boolean indicating whether this {{IntersectionObserver}} will track 327 | changes in a target's visibility. 328 |
329 | 330 | An {{Element}} is defined as having a content clip if its computed style has overflow properties that cause its content to be clipped to the element's padding edge. 331 | 332 | The root intersection rectangle 333 | for an {{IntersectionObserver}} 334 | is the rectangle we'll use to check against the targets. 335 | 336 |
337 |
If the {{IntersectionObserver}} is an implicit root observer, 338 |
it's treated as if the root were the top-level browsing context's {{document}}, according to the following rule for {{document}}. 339 | 340 |
If the intersection root is a {{document}}, 341 |
it's the size of the {{document}}'s viewport (note that this processing step can only be reached if the {{document}} is fully active). 342 | 343 |
Otherwise, if the intersection root has a content clip, 344 |
it's the element's padding area. 345 | 346 |
Otherwise, 347 |
it's the result of getting the bounding box for the intersection root. 348 |
349 | 350 | When calculating the root intersection rectangle for 351 | a same-origin-domain target, the rectangle is then expanded 352 | according to the offsets in the {{IntersectionObserver}}’s {{[[rootMargin]]}} slot 353 | in a manner similar to CSS's 'margin' property, 354 | with the four values indicating the amount the top, right, bottom, and left edges, respectively, are offset by, 355 | with positive lengths indicating an outward offset. 356 | Percentages are resolved relative to the width of the undilated rectangle. 357 | 358 | Note: {{IntersectionObserver/rootMargin}} only applies to the intersection root itself. 359 | If a target {{Element}} is clipped by an ancestor other than the 360 | intersection root, that clipping is unaffected by 361 | {{IntersectionObserver/rootMargin}}. 362 | 363 | : To apply scroll margin to a scrollport 364 | :: 365 | When calculating a scrollport intersection rectangle for 366 | a same-origin-domain target, the rectangle is expanded 367 | according to the offsets in the {{IntersectionObserver}}’s {{[[scrollMargin]]}} slot 368 | in a manner similar to CSS's 'margin' property, 369 | with the four values indicating the amount the top, right, bottom, and left edges, respectively, are offset by, 370 | with positive lengths indicating an outward offset. 371 | Percentages are resolved relative to the width of the undilated rectangle. 372 | 373 | These offsets are only applied when handling same-origin-domain targets; 374 | for cross-origin-domain targets they are ignored. 375 | 376 | Note: {{IntersectionObserver/scrollMargin}} affects the clipping of target 377 | by all scrollable ancestors up to and including the intersection root. 378 | Both the {{IntersectionObserver/scrollMargin}} and the {{IntersectionObserver/rootMargin}} 379 | are applied to a scrollable intersection root's rectangle. 380 | 381 | Note: Root intersection rectangle and scrollport intersection rectangles are not affected by 382 | pinch zoom and will report the unadjusted viewport, consistent with the 383 | intent of pinch zooming (to act like a magnifying glass and NOT change layout.) 384 | 385 | To parse a margin (root or scroll) 386 | from an input string |marginString|, 387 | returning either a list of 4 pixel lengths or percentages, 388 | or failure: 389 | 390 | 1. Parse a list of component values |marginString|, 391 | storing the result as |tokens|. 392 | 2. Remove all whitespace tokens from |tokens|. 393 | 3. If the length of |tokens| is greater than 4, 394 | return failure. 395 | 4. If there are zero elements in |tokens|, 396 | set |tokens| to ["0px"]. 397 | 5. Replace each |token| in |tokens|: 398 | * If |token| is an absolute length dimension token, 399 | replace it with a an equivalent pixel length. 400 | * If |token| is a <> token, 401 | replace it with an equivalent percentage. 402 | * Otherwise, return failure. 403 | 6. If there is one element in |tokens|, 404 | append three duplicates of that element to |tokens|. 405 | Otherwise, if there are two elements are |tokens|, 406 | append a duplicate of each element to |tokens|. 407 | Otherwise, if there are three elements in |tokens|, 408 | append a duplicate of the second element to |tokens|. 409 | 7. Return |tokens|. 410 | 411 |

412 | The IntersectionObserverEntry interface

413 | 414 |
415 | [Exposed=Window]
416 | interface IntersectionObserverEntry {
417 | 	constructor(IntersectionObserverEntryInit intersectionObserverEntryInit);
418 | 	readonly attribute DOMHighResTimeStamp time;
419 | 	readonly attribute DOMRectReadOnly? rootBounds;
420 | 	readonly attribute DOMRectReadOnly boundingClientRect;
421 | 	readonly attribute DOMRectReadOnly intersectionRect;
422 | 	readonly attribute boolean isIntersecting;
423 | 	readonly attribute boolean isVisible;
424 | 	readonly attribute double intersectionRatio;
425 | 	readonly attribute Element target;
426 | };
427 | 
428 | dictionary IntersectionObserverEntryInit {
429 | 	required DOMHighResTimeStamp time;
430 | 	required DOMRectInit? rootBounds;
431 | 	required DOMRectInit boundingClientRect;
432 | 	required DOMRectInit intersectionRect;
433 | 	required boolean isIntersecting;
434 | 	required boolean isVisible;
435 | 	required double intersectionRatio;
436 | 	required Element target;
437 | };
438 | 
439 | 440 |
441 | : boundingClientRect 442 | :: 443 | A {{DOMRectReadOnly}} obtained by getting the bounding box 444 | for {{IntersectionObserverEntry/target}}. 445 | : intersectionRect 446 | :: 447 | {{IntersectionObserverEntry/boundingClientRect}}, intersected by 448 | each of {{IntersectionObserverEntry/target}}'s ancestors' clip 449 | rects (up to but not including {{IntersectionObserver/root}}), 450 | intersected with the root intersection rectangle. 451 | This value represents the portion of 452 | {{IntersectionObserverEntry/target}} that intersects with 453 | the root intersection rectangle. 454 | : isIntersecting 455 | :: 456 | True if the {{IntersectionObserverEntry/target}} intersects with the 457 | {{IntersectionObserver/root}}; false otherwise. This flag makes it 458 | possible to distinguish between an {{IntersectionObserverEntry}} signalling 459 | the transition from intersecting to not-intersecting; and an 460 | {{IntersectionObserverEntry}} signalling a transition from not-intersecting 461 | to intersecting with a zero-area intersection rect (as will happen with 462 | edge-adjacent intersections, or when the {{IntersectionObserverEntry/boundingClientRect}} 463 | has zero area). 464 | : isVisible 465 | :: 466 | Contains the result of running the visibility algorithm 467 | on {{IntersectionObserverEntry/target}}. 468 | : intersectionRatio 469 | :: 470 | If the {{IntersectionObserverEntry/boundingClientRect}} has non-zero area, 471 | this will be the ratio of {{IntersectionObserverEntry/intersectionRect}} area to 472 | {{IntersectionObserverEntry/boundingClientRect}} area. Otherwise, this will be 473 | 1 if the {{IntersectionObserverEntry/isIntersecting}} is true, and 0 if not. 474 | : rootBounds 475 | :: 476 | For a same-origin-domain target, this will be the root intersection rectangle. 477 | Otherwise, this will be null. 478 | Note that if the target is in a different browsing context than the intersection root, 479 | this will be in a different coordinate system 480 | than {{IntersectionObserverEntry/boundingClientRect}} and {{IntersectionObserverEntry/intersectionRect}}. 481 | : target 482 | :: 483 | The {{Element}} whose intersection with the 484 | intersection root changed. 485 | : time 486 | :: 487 | The attribute must return a {{DOMHighResTimeStamp}} 488 | that corresponds to the time the intersection was recorded, relative to the 489 | time origin of the global object associated with the IntersectionObserver instance 490 | that generated the notification. 491 |
492 | 493 |

494 | The IntersectionObserverInit dictionary

495 | 496 |
497 | dictionary IntersectionObserverInit {
498 | 	(Element or Document)?  root = null;
499 | 	DOMString rootMargin = "0px";
500 | 	DOMString scrollMargin = "0px";
501 | 	(double or sequence<double>) threshold = 0;
502 | 	long delay = 0;
503 | 	boolean trackVisibility = false;
504 | };
505 | 
506 | 507 |
508 | : root 509 | :: 510 | The {{IntersectionObserver|root}} to use for intersection. 511 | If not provided, use the implicit root. 512 | : rootMargin 513 | :: 514 | Similar to the CSS 'margin' property, 515 | this is a string of 1-4 components, 516 | each either an absolute length or a percentage. 517 | 518 |
519 | 			"5px"                // all margins set to 5px
520 | 			"5px 10px"           // top & bottom = 5px, right & left = 10px
521 | 			"-10px 5px 8px"      // top = -10px, right & left = 5px, bottom = 8px
522 | 			"-10px -5px 5px 8px" // top = -10px, right = -5px, bottom = 5px, left = 8px
523 | 		
524 | : scrollMargin 525 | :: 526 | Similar to {{IntersectionObserverInit/rootMargin}}, 527 | this is a string of 1-4 components, 528 | each either an absolute length or a percentage. 529 | 530 | See {{IntersectionObserverInit/rootMargin}} above for the example. 531 | : threshold 532 | :: 533 | List of threshold(s) at which to trigger callback. 534 | callback will be invoked when intersectionRect's area changes from 535 | greater than or equal to any threshold to less than that threshold, 536 | and vice versa. 537 | 538 | Threshold values must be in the range of [0, 1.0] and represent a 539 | percentage of the area of the rectangle produced 540 | by getting the bounding box for target. 541 | 542 | Note: 0.0 is effectively "any non-zero number of pixels". 543 | : delay 544 | :: 545 | A number specifying the minimum delay in milliseconds 546 | between notifications from the observer for a given target. 547 | : trackVisibility 548 | :: 549 | A boolean indicating whether the observer should track visibility. 550 | Note that tracking visibility is likely to be a more expensive operation 551 | than tracking intersections. It is recommended that this option be used 552 | only when necessary. 553 |
554 | 555 |

556 | Processing Model

557 | 558 | This section outlines the steps the user agent must take when implementing 559 | the Intersection Observer API. 560 | 561 |

562 | Internal Slot Definitions

563 | 564 |

565 | Document

566 | 567 | Each {{document}} has an 568 | IntersectionObserverTaskQueued flag 569 | which is initialized to false. 570 | 571 |

572 | Element

573 | 574 | {{Element}} objects have an internal 575 | \[[RegisteredIntersectionObservers]] slot, 576 | which is initialized to an empty list. 577 | This list holds IntersectionObserverRegistration records, 578 | which have: 579 | * an observer property 580 | holding an {{IntersectionObserver}}. 581 | * a previousThresholdIndex property 582 | holding a number between -1 and the length of the observer's {{IntersectionObserver/thresholds}} property (inclusive). 583 | * a previousIsIntersecting property 584 | holding a boolean. 585 | * a lastUpdateTime property 586 | holding a {{DOMHighResTimeStamp}} value. 587 | * a previousIsVisible property 588 | holding a boolean. 589 | 590 |

591 | IntersectionObserver

592 | 593 | {{IntersectionObserver}} objects have the following internal slots: 594 | * A \[[QueuedEntries]] slot 595 | initialized to an empty list. 596 | * A \[[ObservationTargets]] slot 597 | initialized to an empty list. 598 | * A \[[callback]] slot 599 | which is initialized by {{IntersectionObserver(callback, options)}}. 600 | * A \[[rootMargin]] slot 601 | which is a list of four pixel lengths or percentages. 602 | * A \[[scrollMargin]] slot 603 | which is a list of four pixel lengths or percentages. 604 | * A \[[thresholds]] slot 605 | which is initialized by {{IntersectionObserver(callback, options)}}. 606 | * A \[[delay]] slot 607 | which is initialized by {{IntersectionObserver(callback, options)}}. 608 | * A \[[trackVisibility]] slot 609 | which is initialized by {{IntersectionObserver(callback, options)}}. 610 | 611 |

612 | Algorithms

613 | 614 |

Initialize a new IntersectionObserver

615 | 616 | To initialize a new IntersectionObserver, given an {{IntersectionObserverCallback}} |callback| 617 | and an {{IntersectionObserverInit}} dictionary |options|, run these steps: 618 | 619 | 1. Let |this| be a new {{IntersectionObserver}} object 620 | 2. Set |this|'s internal {{[[callback]]}} slot to |callback|. 621 | 3. Attempt to parse a margin 622 | from |options|.{{IntersectionObserverInit/rootMargin}}. 623 | If a list is returned, 624 | set |this|'s internal {{[[rootMargin]]}} slot to that. 625 | Otherwise, throw a {{SyntaxError}} exception. 626 | 4. Attempt to parse a margin 627 | from |options|.{{IntersectionObserverInit/scrollMargin}}. 628 | If a list is returned, 629 | set |this|'s internal {{[[scrollMargin]]}} slot to that. 630 | Otherwise, throw a {{SyntaxError}} exception. 631 | 5. Let |thresholds| be a list equal to 632 | |options|.{{IntersectionObserverInit/threshold}}. 633 | 6. If any value in |thresholds| is less than 0.0 or greater than 634 | 1.0, throw a {{RangeError}} exception. 635 | 7. Sort |thresholds| in ascending order. 636 | 8. If |thresholds| is empty, append 0 to |thresholds|. 637 | 9. The {{IntersectionObserver/thresholds}} attribute getter will return 638 | this sorted |thresholds| list. 639 | 10. Let |delay| be the value of |options|.{{IntersectionObserverInit/delay}}. 640 | 11. If |options|.{{IntersectionObserverInit/trackVisibility}} is true 641 | and |delay| is less than 100, set |delay| to 100. 642 | 11. Set |this|'s internal {{[[delay]]}} slot to |options|.{{IntersectionObserverInit/delay}} to |delay|. 643 | 12. Set |this|'s internal {{[[trackVisibility]]}} slot to |options|.{{IntersectionObserverInit/trackVisibility}}. 644 | 13. Return |this|. 645 | 646 |

Observe a target Element

647 | 648 | To observe a target Element, given an {{IntersectionObserver}} |observer| 649 | and an {{Element}} |target|, follow these steps: 650 | 651 | 1. If |target| is in |observer|'s internal {{[[ObservationTargets]]}} slot, 652 | return. 653 | 2. Let |intersectionObserverRegistration| be 654 | an {{IntersectionObserverRegistration}} record 655 | with an {{IntersectionObserverRegistration/observer}} property set to |observer|, 656 | a {{IntersectionObserverRegistration/previousThresholdIndex}} property set to -1, 657 | a {{IntersectionObserverRegistration/previousIsIntersecting}} property set to false, 658 | and a {{IntersectionObserverRegistration/previousIsVisible}} property set to false. 659 | 3. Append |intersectionObserverRegistration| 660 | to |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot. 661 | 4. Add |target| to |observer|'s internal {{[[ObservationTargets]]}} slot. 662 | 663 |

Unobserve a target Element

664 | 665 | To unobserve a target Element, given an {{IntersectionObserver}} |observer| 666 | and an {{Element}} |target|, follow these steps: 667 | 668 | 1. Remove the {{IntersectionObserverRegistration}} record 669 | whose {{IntersectionObserverRegistration/observer}} property is equal to |this| 670 | from |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot, if present. 671 | 2. Remove |target| from |this|'s internal {{[[ObservationTargets]]}} slot, if present 672 | 673 |

674 | Queue an Intersection Observer Task

675 | 676 | The IntersectionObserver task source is a [=task source=] used for 677 | scheduling tasks to [[#notify-intersection-observers-algo]]. 678 | 679 | To queue an intersection observer task for a {{document}} |document|, 680 | run these steps: 681 | 682 | 1. If |document|'s IntersectionObserverTaskQueued flag is set to true, 683 | return. 684 | 2. Set |document|'s IntersectionObserverTaskQueued flag to true. 685 | 3. Queue a task on the IntersectionObserver task source 686 | associated with the {{document}}'s event loop 687 | to notify intersection observers. 688 | 689 |

690 | Notify Intersection Observers

691 | 692 | To notify intersection observers for a {{document}} |document|, 693 | run these steps: 694 | 695 | 1. Set |document|'s IntersectionObserverTaskQueued flag to false. 696 | 2. Let |notify list| be a list of all {{IntersectionObserver}}s 697 | whose {{IntersectionObserver/root}} is in the DOM tree of |document|. 698 | 3. For each {{IntersectionObserver}} object |observer| in |notify list|, 699 | run these steps: 700 | 701 | 1. If |observer|'s internal {{[[QueuedEntries]]}} slot is empty, 702 | continue. 703 | 2. Let |queue| be a copy of |observer|'s internal {{[[QueuedEntries]]}} 704 | slot. 705 | 3. Clear |observer|'s internal {{[[QueuedEntries]]}} slot. 706 | 4. Let |callback| be the value of |observer|'s internal {{[[callback]]}} slot. 707 | 5. Invoke |callback| with |queue| as the first argument, |observer| as the second argument, 708 | and |observer| as the callback this value. 709 | If this throws an exception, report the exception. 710 | 711 |

712 | Queue an IntersectionObserverEntry

713 | 714 | To queue an IntersectionObserverEntry for an {{IntersectionObserver}} |observer|, given a 715 | {{document}} |document|; {{DOMHighResTimeStamp}} |time|; 716 | {{DOMRect}}s |rootBounds|, |boundingClientRect|, |intersectionRect|, and |isIntersecting| flag; 717 | and an {{Element}} |target|; 718 | run these steps: 719 | 720 | 1. Construct an {{IntersectionObserverEntry}}, passing in |time|, |rootBounds|, 721 | |boundingClientRect|, |intersectionRect|, |isIntersecting|, and |target|. 722 | 2. Append it to |observer|'s internal {{[[QueuedEntries]]}} slot. 723 | 3. Queue an intersection observer task for |document|. 724 | 725 |

726 | Compute the Intersection of a Target Element and the Root

727 | 728 | To compute the intersection between a target |target| and an intersection root |root|, 729 | run these steps: 730 | 731 | 1. Let |intersectionRect| be the result of getting the bounding box for |target|. 732 | 2. Let |container| be the containing block of |target|. 733 | 3. While |container| is not |root|: 734 | 1. If |container| is the {{document}} of a nested browsing context, 735 | update |intersectionRect| by clipping to the viewport 736 | of the {{document}}, and update |container| to be 737 | the browsing context container of |container|. 738 | 2. Map |intersectionRect| to the coordinate space of |container|. 739 | 3. If |container| is a scroll container, apply the {{IntersectionObserver}}’s 740 | {{[[scrollMargin]]}} to the |container|'s clip rect as described in apply scroll margin to a scrollport. 741 | 4. If |container| has a content clip or a css clip-path property, 742 | update |intersectionRect| by applying |container|'s clip. 743 | 5. If |container| is the root element of a browsing context, 744 | update |container| to be the browsing context's {{document}}; 745 | otherwise, update |container| to be the containing block 746 | of |container|. 747 | 4. Map |intersectionRect| to the coordinate space of |root|. 748 | 5. Update |intersectionRect| by intersecting it with the root intersection rectangle. 749 | 6. Map |intersectionRect| to the coordinate space of the viewport of the {{document}} containing |target|. 750 | 7. Return |intersectionRect|. 751 | 752 |

753 | Compute whether a Target is unoccluded, untransformed, unfiltered, and opaque.

754 | 755 | To compute the visibility of a target, run these steps: 756 | 1. If the |observer|'s {{IntersectionObserver/trackVisibility}} attribute is false, return false. 757 | 2. If the target has an effective transformation matrix other than a 2D translation or proportional 2D upscaling, return false. 758 | 3. If the target, or any element in its containing block chain, has an effective opacity other than 100%, return false. 759 | 4. If the target, or any element in its containing block chain, has any filters applied, return false. 760 | 5. If the implementation cannot guarantee that the target is completely unoccluded by other page content, return false. 761 | 762 | Note: Implementations should use the ink overflow rectangle of page content when determining whether a target is occluded. For blur effects, which have theoretically infinite extent, the ink overflow rectangle is defined by the finite-area approximation described for the blur filter function. 763 | 764 | 6. Return true. 765 | 766 |

Calculate a target's Effective Transformation Matrix

767 | To compute the effective transformation matrix of a target, run these steps: 768 | 1. Let |matrix| be the serialization of the identity transform function. 769 | 2. Let |container| be the target. 770 | 3. While |container| is not the intersection root: 771 | 1. Set |t| to |container|'s transformation matrix. 772 | 2. Set |matrix| to |t| post-multiplied by |matrix|. 773 | 3. If |container| is the root element of a nested browsing context, 774 | update |container| to be the browsing context container of |container|. Otherwise, update |container| to be the containing block of |container|. 775 | 4. Return |matrix|. 776 | 777 | 778 |

779 | Run the Update Intersection Observations Steps

780 | 781 | To run the update intersection observations steps for a 782 | Document |document| given a timestamp |time|, run these steps: 783 | 784 | 1. Let |observer list| be a list of all {{IntersectionObserver}}s 785 | whose {{IntersectionObserver/root}} is in the DOM tree of |document|. 786 | For the top-level browsing context, this includes implicit root observers. 787 | 2. For each |observer| in |observer list|: 788 | 1. Let |rootBounds| be |observer|'s root intersection rectangle. 789 | 2. For each |target| in |observer|'s internal {{[[ObservationTargets]]}} slot, processed in the same order that {{observe()}} was called on each |target|: 790 | 1. Let |registration| be the {{IntersectionObserverRegistration}} record 791 | in |target|'s internal {{[[RegisteredIntersectionObservers]]}} slot 792 | whose {{IntersectionObserverRegistration/observer}} property is equal to |observer|. 793 | 2. If (|time| - |registration|.{{IntersectionObserverRegistration/lastUpdateTime}} < |observer|.{{IntersectionObserver/delay}}), skip further processing for |target|. 794 | 3. Set |registration|.{{IntersectionObserverRegistration/lastUpdateTime}} to |time|. 795 | 4. Let: 796 | - |thresholdIndex| be 0. 797 | - |isIntersecting| be false. 798 | - |targetRect| be a {{DOMRectReadOnly}} with |x|, |y|, |width|, and |height| set to 0. 799 | - |intersectionRect| be a {{DOMRectReadOnly}} with |x|, |y|, |width|, and |height| set to 0. 800 | 5. If the intersection root is not the implicit root, 801 | and |target| is not in the same {{document}} as the intersection root, 802 | skip to step 11. 803 | 6. If the intersection root is an {{Element}}, 804 | and |target| is not a descendant of the intersection root 805 | in the containing block chain, skip to step 11. 806 | 7. Set |targetRect| to the {{DOMRectReadOnly}} obtained by getting the bounding box for 807 | |target|. 808 | 8. Let |intersectionRect| be the result of running the compute the intersection 809 | algorithm on |target| and |observer|'s intersection root. 810 | 9. Let |targetArea| be |targetRect|'s area. 811 | 10. Let |intersectionArea| be |intersectionRect|'s area. 812 | 11. Let |isIntersecting| be true if |targetRect| and |rootBounds| intersect or are edge-adjacent, 813 | even if the intersection has zero area (because |rootBounds| or |targetRect| have 814 | zero area). 815 | 12. If |targetArea| is non-zero, let |intersectionRatio| be |intersectionArea| divided by |targetArea|.
816 | Otherwise, let |intersectionRatio| be 1 if |isIntersecting| is true, or 0 if |isIntersecting| is false. 817 | 13. Set |thresholdIndex| to the index of the first entry in |observer|.{{thresholds}} whose value is greater than |intersectionRatio|, or the length of |observer|.{{thresholds}} if |intersectionRatio| is greater than or equal to the last entry in |observer|.{{thresholds}}. 818 | 14. Let |isVisible| be the result of running the visibility algorithm on |target|. 819 | 15. Let |previousThresholdIndex| be the |registration|'s 820 | {{IntersectionObserverRegistration/previousThresholdIndex}} property. 821 | 16. Let |previousIsIntersecting| be the |registration|'s 822 | {{IntersectionObserverRegistration/previousIsIntersecting}} property. 823 | 17. Let |previousIsVisible| be the |registration|'s 824 | {{IntersectionObserverRegistration/previousIsVisible}} property. 825 | 18. If |thresholdIndex| does not equal |previousThresholdIndex|, 826 | or if |isIntersecting| does not equal |previousIsIntersecting|, 827 | or if |isVisible| does not equal |previousIsVisible|, 828 | queue an IntersectionObserverEntry, 829 | passing in |observer|, |time|, |rootBounds|, 830 | |targetRect|, |intersectionRect|, |isIntersecting|, 831 | |isVisible|, and |target|. 832 | 19. Assign |thresholdIndex| to |registration|'s 833 | {{IntersectionObserverRegistration/previousThresholdIndex}} property. 834 | 20. Assign |isIntersecting| to |registration|'s 835 | {{IntersectionObserverRegistration/previousIsIntersecting}} property. 836 | 21. Assign |isVisible| to |registration|'s 837 | {{IntersectionObserverRegistration/previousIsVisible}} property. 838 | 839 |

840 | IntersectionObserver Lifetime

841 | 842 | An {{IntersectionObserver}} will remain alive until both of these conditions hold: 843 | 847 | 848 | An {{IntersectionObserver}} will continue observing a target until either the observer's {{IntersectionObserver/unobserve()}} method is called with the target as argument; or the observer's {{IntersectionObserver/disconnect()}} is called. 849 | 850 |

851 | External Spec Integrations

852 | 853 |

854 | HTML Processing Model: Event Loop

855 | 856 | An Intersection Observer processing step exists as a substep 857 | within the "Update the rendering" step, 858 | in the HTML Event Loops Processing Model. 859 | 860 |

Pending initial IntersectionObserver targets

861 | A {{document}} is said to have pending initial IntersectionObserver targets 862 | if there is at least one {{IntersectionObserver}} meeting these criteria: 863 |
    864 |
  1. The |observer|'s {{IntersectionObserver|root}} is in the |document| 865 | (for the top-level browsing context, this includes implicit root observers). 866 |
  2. 867 |
  3. The |observer| has at least one |target| in its {{[[ObservationTargets]]}} slot for which 868 | no {{IntersectionObserverEntry}} has yet been queued. 869 |
  4. 870 |
871 | 872 | In the HTML Event Loops Processing Model, under the "Update the rendering" step, the "Unnecessary rendering" step should be modified to add an additional requirement for skipping the rendering update: 873 | 874 | 875 | 876 | 877 |

Accessibility Considerations

878 | 879 | 880 | *This section is non-normative.* 881 | 882 | There are no known accessibility considerations for the core IntersectionObserver specification (this document). 883 | There are, however, related specifications and proposals that leverage and refer to this spec, 884 | which might have their own accessibility considerations. In particular, specifications for 885 | [[html#lazy-loading-attributes]] and [[css-contain-2#content-visibility]] 886 | may have implications for [[html#find-in-page]], [[html#the-tabindex-attribute]], and 887 | spatial navigation. 888 | 889 | 890 |

Privacy and Security

891 | 892 | 893 | *This section is non-normative.* 894 | 895 | The main privacy concerns associated with this API relate to the information 896 | it may provide to code running in the context of a cross-origin iframe 897 | (i.e., the cross-origin-domain target case). In particular: 898 | 899 | * There is no universal consensus on the privacy implications of 900 | revealing whether an iframe is within the global viewport. 901 | 902 | * There is a risk that the API may be used to probe for information 903 | about the geometry of the global viewport itself, 904 | which may be used to deduce the user's hardware configuration. 905 | The motivation for disabling the effects of {{IntersectionObserver/rootMargin}} 906 | and {{IntersectionObserver/scrollMargin}}, and suppressing {{IntersectionObserverEntry/rootBounds}} 907 | for cross-origin-domain targets is to prevent such probing. 908 | 909 | It should be noted that prior to {{IntersectionObserver}}, web developers 910 | used other API's in very ingenious (and grotesque) ways to tease out the information 911 | available from {{IntersectionObserver}}. As a purely practical matter, this API 912 | does not reveal any information that was not already available by other means. 913 | 914 | Another consideration is that {{IntersectionObserver}} uses {{DOMHighResTimeStamp}}, 915 | which has privacy and security considerations of its own. It is however unlikely that 916 | {{IntersectionObserver}} is vulnerable to timing-related exploits. Timestamps are generated 917 | at most once per rendering update (see [[#event-loop]]), which is far too 918 | infrequent for the familiar kind of timing attack. 919 | 920 | 921 |

Internationalization

922 | 923 | 924 | *This section is non-normative.* 925 | 926 | There are no known issues concerning internationalization. 927 | 928 | 929 |

Acknowledgements

930 | 931 | 932 | Special thanks to all the contributors for their technical input and suggestions that led to improvements to this 933 | specification. 934 | --------------------------------------------------------------------------------