{"id":296169,"date":"2019-09-24T07:18:35","date_gmt":"2019-09-24T14:18:35","guid":{"rendered":"https:\/\/css-tricks.com\/?p=296169"},"modified":"2019-09-24T15:01:56","modified_gmt":"2019-09-24T22:01:56","slug":"an-explanation-of-how-the-intersection-observer-watches","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/an-explanation-of-how-the-intersection-observer-watches\/","title":{"rendered":"An Explanation of How the Intersection Observer Watches"},"content":{"rendered":"

There have been several excellent articles exploring how to use this API, including choices from authors such as Phil Hawksworth<\/a>, Preethi<\/a>, and Mateusz Rybczonek<\/a>, just to name a few. But I\u2019m aiming to do something a bit different here. I had an opportunity earlier in the year to present the VueJS transition component to the Dallas VueJS Meetup of which my first article<\/a> on CSS-Tricks was based on. During the question-and-answer session of that presentation I was asked about triggering the transitions based on scroll events — which of course you can, but it was suggested by a member of the audience to look into the Intersection Observer.<\/p>\n

This got me thinking. I knew the basics of Intersection Observer and how to make a simple example of using it. Did I know how to explain not only how to use it<\/em> but how it works<\/em>? What exactly does it provide to us as developers? As a “senior” dev, how would I explain it to someone right out of a bootcamp who possibly doesn\u2019t even know it exists?<\/p>\n

I decided I needed to know. After spending some time researching, testing, and experimenting, I\u2019ve decided to share a good bit of what I\u2019ve learned. Hence, here we are.<\/p>\n

<\/p>\n

A brief explanation of the Intersection Observer<\/h3>\n

The abstract of the W3C public working draft<\/a> (first draft dated September 14, 2017) describes the Intersection Observer API as:<\/p>\n

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.<\/p><\/blockquote>\n

The general idea being a way to watch a child element and be informed when it enters the bounding box of one of its parents. This is most commonly going to be used in relation to the target element scrolling into view in the root element. Up until the introduction of the Intersection Observer, this type of functionality was accomplished by listening for scroll events.<\/p>\n

Although the Intersection Observer is a more performant solution for this type of functionality, I do not suggest we necessarily look at it as a replacement to scroll events. Instead, I suggest we look at this API as an additional tool that has a functional overlap with scroll events. In some cases, the two can work together to solve specific problems.<\/p>\n

A basic example<\/h3>\n

I know I risk repeating what’s already been explained in other articles, but let\u2019s see a basic example of an Intersection Observer and what it gives us.<\/p>\n

The Observer is made up of four parts:<\/p>\n

    \n
  1. the “root,” which is the parent element the observer is tied to, which can be the viewport<\/li>\n
  2. the “target,” which is a child element being observed and there can be more than one<\/li>\n
  3. the options object, which defines certain aspects of the observer\u2019s behavior<\/li>\n
  4. the callback function, which is invoked each time an intersection change is observed<\/li>\n<\/ol>\n

    The code of a basic example could look something like this:<\/p>\n

    const options = {\r\n  root: document.body,\r\n  rootMargin: '0px',\r\n  threshold: 0\r\n}\r\n\r\nfunction callback (entries, observer) {\r\n  console.log(observer);\r\n  \r\n  entries.forEach(entry => {\r\n    console.log(entry);\r\n  });\r\n}\r\n\r\nlet observer = new IntersectionObserver(callback, options);\r\nobserver.observe(targetElement);<\/code><\/pre>\n

    The first section in the code is the options object which has root<\/code>, rootMargin<\/code>, and threshold<\/code> properties.<\/p>\n

    The root<\/code> is the parent element, often a scrolling element, that contains the observed elements. This can be just about any single element on the page as needed. If the property isn\u2019t provided at all or the value is set to null, the viewport is set to be the root element.<\/p>\n

    The rootMargin<\/code> is a string of values describing what can be called the margin of the root element, which affects the resulting bounding box that the target element scrolls into. It behaves much like the CSS margin<\/a><\/code> property. You can have values like 10px 15px 20px<\/code> which gives us a top margin of 10px, left and right margins of 15px, and a bottom margin of 20px. Only the bounding box is affected and not the element itself. Keep in mind that the only lengths allowed are pixels and percentage values, which can be negative or positive. Also note that the rootMargin<\/code> does not work if the root element is not an actual element on the page, such as the viewport.<\/p>\n

    The threshold<\/code> is the value used to determine when an intersection change should be observed. More than one value can be included in an array so that the same target can trigger the intersection multiple times. The different values are a percentage using zero to one, much like opacity<\/a><\/code> in CSS, so a value of 0.5 would be considered 50% and so on. These values relate to the target\u2019s intersection ratio, which will be explained in just a moment. A threshold of zero triggers the intersection when the first pixel of the target element intersects the root element. A threshold of one triggers when the entire target element is inside the root element.<\/p>\n

    The second section in the code is the callback function that is called whenever a intersection change is observed. Two parameters are passed; the entries are stored in an array and represent each target element that triggers the intersection change. This provides a good bit of information that can be used for the bulk of any functionality that a developer might create. The second parameter is information about the observer itself, which is essentially the data from the provided options<\/code> object. This provides a way to identify which observer is in play in case a target is tied to multiple observers.<\/p>\n

    The third section in the code is the creation of the observer itself and where it is observing the target. When creating the observer, the callback function and options<\/code> object can be external to the observer, as shown. A developer could write the code inline, but the observer is very flexible. For example, the callback and options can be used across multiple observers, if needed. The observe()<\/code> method is then passed the target element that needs to be observed. It can only accept one target but the method can be repeated on the same observer for multiple targets. Again, very flexible.<\/p>\n

    Notice the console logs in the code. Here is what those output.<\/p>\n

    The observer object<\/h4>\n

    Logging the observer data passed into the callback gets us something like this:<\/p>\n

    IntersectionObserver\r\n  root: null\r\n  rootMargin: \"0px 0px 0px 0px\"\r\n  thresholds: Array [ 0 ]\r\n  <prototype>: IntersectionObserverPrototype { }<\/code><\/pre>\n

    …which is essentially the options<\/code> object passed into the observer when it was created. This can be used to determine the root element that the intersection is tied to. Notice that even though the original options<\/code> object had 0px as the rootMargin<\/code>, this object reports it as 0px 0px 0px 0px<\/code>, which is what would be expected when considering the rules of CSS margins. Then there\u2019s the array of thresholds the observer is operating under.<\/p>\n

    The entry object<\/h4>\n

    Logging the entry data passed into the callback gets us something like this:<\/p>\n

    IntersectionObserverEntry\r\n  boundingClientRect: DOMRect\r\n    bottom: 923.3999938964844, top: 771\r\n    height: 152.39999389648438, width: 411\r\n    left: 9, right: 420\r\n    x: 9, y: 771\r\n    <prototype>: DOMRectPrototype { }\r\n  intersectionRatio: 0\r\n  intersectionRect: DOMRect\r\n    bottom: 0, top: 0\r\n    height: 0, width: 0\r\n    left: 0, right: 0\r\n    x: 0, y: 0\r\n    <prototype>: DOMRectPrototype { }\r\n  isIntersecting: false\r\n  rootBounds: null\r\n  target: <div class=\"item\">\r\n  time: 522\r\n  <prototype>: IntersectionObserverEntryPrototype { }<\/code><\/pre>\n

    Yep, lots of things going on here.<\/p>\n

    For most devs, the two properties that are most likely to be useful are intersectionRatio<\/code> and isIntersecting<\/code>. The isIntersecting<\/code> property is a boolean that is exactly what one might think it is — the target element is intersecting the root element at the time of the intersection change. The intersectionRatio<\/code> is the percentage of the target element that is currently intersecting the root element. This is represented by a percentage of zero to one, much like the threshold provided in the observer\u2019s option object.<\/p>\n

    Three properties — boundingClientRect<\/code>, intersectionRect<\/code>, and rootBounds<\/code> — represent specific data about three aspects of the intersection. The boundingClientRect<\/code> property provides the bounding box of the target element with bottom, left, right, and top values from the top-left of the viewport, just like with Element.getBoundingClientRect()<\/a><\/code>. Then the height and width of the target element is provided as the X and Y coordinates. The rootBounds<\/code> property provides the same form of data for the root element. The intersectionRect<\/code> provides similar data but its describing the box formed by the intersection area of the target element inside the root element, which corresponds to the intersectionRatio<\/code> value. Traditional scroll events would require this math to be done manually.<\/p>\n

    One thing to keep in mind is that all these shapes that represent the different elements are always rectangles. No matter the actual shape of the elements involved, they are always reduced down to the smallest rectangle containing the element.<\/p>\n

    The target<\/code> property refers to the target element that is being observed. In cases where an observer contains multiple targets, this is the easy way to determine which target element triggered this intersection change.<\/p>\n

    The time<\/code> property provides the time (in milliseconds) from when the observer is first created to the time this intersection change is triggered. This is how you can track the time it takes for a viewer to come across a particular target. Even if the target is scrolled into view again at a later time, this property will have the new time provided. This can be used to track the time of a target entering and leaving the root element.<\/p>\n

    While all this information is provided to us whenever an intersection change is observed, it’s also provided to us when the observer is first started. For example, on page load the observers on the page will immediately invoke the callback function and provide the current state of every target element it is observing.<\/p>\n

    This is a wealth of data about the relationships of elements on the page provided in a very performant way.<\/p>\n

    Intersection Observer methods<\/h3>\n

    Intersection Observer has three methods of note: observe()<\/code>, unobserve()<\/code>, and disconnect()<\/code>.<\/p>\n