{"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 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 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 The code of a basic example could look something like this:<\/p>\n The first section in the code is the options object which has The The The 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 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 Notice the console logs in the code. Here is what those output.<\/p>\n Logging the observer data passed into the callback gets us something like this:<\/p>\n …which is essentially the Logging the entry data passed into the callback gets us something like this:<\/p>\n Yep, lots of things going on here.<\/p>\n For most devs, the two properties that are most likely to be useful are Three properties — 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 The 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 has three methods of note: These methods provide the ability to watch and unwatch target elements, but there\u2019s no way to change the options passed to the observer when once it is created. You\u2019ll have to manually recreate the observer if different options are required.<\/p>\n In my exploration of the Intersection Observer and how it compares to using scroll events, I knew that I needed to do some performance testing. A totally unscientific effort was thus created using Puppeteer<\/a>. For the sake of time, I only wanted a general idea of what the performance difference is between the two. Therefore, three simple tests were created.<\/p>\n First, I created a baseline HTML file that included one hundred divs with a bit of height to create a long scrolling page. With a basic http-server active, I loaded the HTML file with Puppeteer, started a trace, forced the page to scroll downward in preset increments to the bottom, stopped the trace once the bottom is reached, and finally saved the results of the trace. I also made it so the test can be repeated multiple times and output data each time. Then I duplicated the baseline HTML and wrote my JavaScript in a script tag for each type of test I wanted to run. Each test has two files: one for the Intersection Observer and the other for scroll events.<\/p>\n The purpose of all the tests is to detect when a target element scrolls upward through the viewport at 25% increments. At each increment, a CSS class is applied that changes the background color of the element. In other words, each element has DOM changes applied to it that would cause repaints. Each test was run five times on two different machines: my development Mac that\u2019s rather up-to-date hardware-wise and my personal Windows 7 machine that\u2019s probably average these days. The results of the trace summary of scripting, rendering, painting, and system were recorded and then averaged. Again, nothing too scientific about all this — just a general idea.<\/p>\n The first test has one observer or one scroll event with one callback each. This is a fairly standard setup for both the observer and scroll event. Although, in this case, the scroll event has a bit more work to do because it attempts to mimic the data that the observer provides by default. Once all those calculations are done, the data is stored in an entry array just like the observer does. Then the functionality for removing and applying classes between the two is exactly the same. I do throttle the scroll event a bit with The second test has 100 observers or 100 scroll events with one callback for each type. Each element is assigned its own observer and event but the callback function is the same. This is actually inefficient because each observer and event behaves exactly the same, but I wanted a simple stress test without having to create 100 unique observers and events \u2014 though I have seen many examples of using the observer this way.<\/p>\n The third test has 100 observers or 100 scroll events with 100 callbacks for each type. This means each element has its own observer, event, and callback function. This, of course, is horribly inefficient since this is all duplicated functionality stored in huge arrays. But this inefficiency is the point of this test.<\/p>\n In the charts above, you\u2019ll see the first column represents our baseline where no JavaScript was run at all. The next two columns represent the first type of test. The Mac ran both quite well as I would expect for a top-end machine for development. The Windows machine gave us a different story. For me, the main point of interest is the scripting results in red. On the Mac, the difference was around 88ms for the observer while around 300ms for the scroll event. The overall result on the Mac is fairly close for each but that scripting took a beating with the scroll event. For the Windows machine its far, far worse. The observer was around 150ms versus around 1400ms for the first and easiest test of the three.<\/p>\n For the second test, we start to see the inefficiency of the scroll test made clearer. Both the Mac and Windows machines ran the observer test with much the same results as before. For the scroll event test, the scripting gets more bogged down to complete the tasks given. The Mac jumped to almost a full second of scripting while the Windows machine jumped approximately to a staggering 3200ms.<\/p>\n For the third test, things thankfully did not get worse. The results are roughly the same as the second test. One thing to note is that across all three tests the results for the observer were consistent for both computers. Despite no efforts in efficiency for the observer tests, the Intersection Observer outperformed the scroll events by a strong margin.<\/p>\n So, after my non-scientific testing on my own two machines, I felt I had a decent idea of the differences in performance between scroll events and the Intersection Observer. I\u2019m sure with some effort I could make the scroll events more efficient but is it worth doing? There are cases where the precision of the scroll events is necessary, but in most cases, the Intersection Observer will suffice nicely — especially since it appears to be far more efficient with no effort at all.<\/p>\n The Take this demo for instance:<\/p>\n \n See the Pen In this demo, the observer has been assigned the parent container as the root element. The child element with the target background has been assigned as the target element. The threshold array has been created with 100 entries with the sequence 0, 0.01, 0.02, 0.03, and so on, until 1. The observer triggers every one percent of the target element appearing or disappearing inside the root element so that, whenever the ratio changes by at least one percent, the output text below the box is updated. In case you\u2019re curious, this threshold was accomplished with this code:<\/p>\n I don\u2019t recommend you set your thresholds in this way for typical use in projects.<\/p>\n At first, the target element is completely contained within the root element and the output above the buttons will show a ratio of one. It should be one on first load but we\u2019ll soon see that the ratio is not always precise; it\u2019s possible the number will be somewhere between 0.99 and 1. That does seem odd, but it can happen, so keep that in mind if you create any checks against the ratio equaling a particular value.<\/p>\n Clicking the “left” button will cause the target element to be transformed to the left so that half of it is in the root element and the other half is out. The Clicking the “top” button does much the same. It transforms the target element to the top of the root element with half of it in and half of it out again. And again, the Clicking the “corner” button again transforms the target element to the upper-right corner of the root element. At this point only a quarter of the target element is within the root element. The If we click the “large” button, that changes the height of the target element to be taller than the root element. The So then, how do we know where the target element is in relation to the root element? Thankfully, the data for this calculation is provided by Consider this demo:<\/p>\n \n See the Pen The setup for this demo is much the same as the one before. The parent container is the root element and the child inside with the target background is the target element. The threshold is an array of 0, 0.5, and 1. As you scroll inside the root element, the target will appear and its position will be reported in the output above the buttons.<\/p>\n Here’s the code that performs these checks:<\/p>\n I should point out that I\u2019m not looping over the entries array as I know there will always only be one entry because there\u2019s only one target. I\u2019m taking a shortcut by making use of You\u2019ll see that a ratio of zero puts the target on the “outside.” A ratio of less than one puts it either at the top or bottom. That lets us see if the target\u2019s “top” is less than the This is part of the efficiency of using the Intersection Observer. Developers don\u2019t need to request this information from various places on a throttled scroll event (which fires quite a lot regardless) and then calculate the related math to figure all this out. It\u2019s provided by the observer and all that\u2019s needed is a simple At first, the target element is taller than the root element, so it is never reported as being “inside.” Click the “toggle target size” button to make it smaller than the root. Now, the target element can be inside the root element when scrolling up and down.<\/p>\n Restore the target element to its original size by clicking on “toggle target size” again, then click on the “toggle root size” button. This resizes the root element so that it is taller than the target element. Once again, while scrolling up and down, it is possible for the target element to be “inside” the root element.<\/p>\n This demo demonstrates two things about the Intersection Observer: how to determine the position of the target element in relation to the root element and what happens when resizing the two elements. This reaction to resizing is another advantage over scroll events \u2014 no need for code to adjust to a resize event.<\/p>\n The “sticky” value for the CSS I\u2019ve seen examples of having an event of sorts for sticky positioning using both scroll events and the Intersection Observer. The solutions using scroll events always have issues similar to using scroll events for other purposes. The usual solution with an observer is with a “dummy” element that serves little purpose other than being a target for the observer. I like to avoid using single purpose elements like that, so I decided to tinker with this particular idea.<\/p>\n In this demo, scroll up and down to see the section titles reacting to being “sticky” to their respective sections.<\/p>\n \n See the Pen This is an example of detecting when a sticky element is at the top of the scrolling container so a class name can be applied to the element. This is accomplished by making use of an interesting quirk of the DOM when giving a specific This pushes the bottom margin of the root\u2019s boundary to the top of the root element, which leaves a small sliver of area available for intersection detection that’s zero pixels. A target element touching this zero pixel area triggers the intersection change, even though it doesn\u2019t exist by the numbers, so to speak. Consider that we can have elements in the DOM that exist with a collapsed height of zero.<\/p>\n This solution takes advantage of this by recognizing the sticky element is always in its “sticky” position at the top of the root element. As scrolling continues, the sticky element eventually moves out of view and the intersection stops. Therefore, we add and remove the class based on the Here\u2019s the HTML:<\/p>\n The outer div with the class Here\u2019s the CSS:<\/p>\n You\u2019ll see that The JavaScript:<\/p>\n This is actually a very straightforward example of using the Intersection Observer for this task. The only oddity is the -100% value in the The limitation of this is that the top, right, bottom, or left property for the sticky element must always be zero. Technically, you could use a different value but then you\u2019d have to do the math to figure out the proper value for the As we’ve seen in some of the demos so far, the \n See the Pen In this demo, we’ve created an Intersection Observer and the callback function serves the sole purpose of adding and removing an event listener that listens for the scroll event on the root element. When the target first enters the root element, the scroll event listener is created and then is removed when the target leaves the root. As the scrolling happens, the output simply shows each event\u2019s timestamp to show it changing in real time \u2014 far more precise than the observer alone.<\/p>\n The setup for the HTML and CSS is fairly standard at this point, so here\u2019s the JavaScript.<\/p>\n This is a fairly standard example. Take note that we\u2019ll want the threshold to be zero because we\u2019ll get multiple event listeners at the same time if there\u2019s more than one threshold. The callback function is what we\u2019re interested in and even that is a simple setup: add and remove the event listener in an This gives the benefits of both Intersection Observer and scroll events. Consider having a scrolling animation library in place that works only when the section of the page that requires it is actually visible. The library and scroll events are not inefficiently active throughout the entire page.<\/p>\n You’re probably wondering how much browser support there is for Intersection Observer. Quite a bit, actually!<\/p>\n This browser support data is from Caniuse<\/a>, which has more detail. A number indicates that browser supports the feature at that version and up.<\/p><\/div>A brief explanation of the Intersection Observer<\/h3>\n
A basic example<\/h3>\n
\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
root<\/code>,
rootMargin<\/code>, and
threshold<\/code> properties.<\/p>\n
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
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
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
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
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
The observer object<\/h4>\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
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
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
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
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
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
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
Intersection Observer methods<\/h3>\n
observe()<\/code>,
unobserve()<\/code>, and
disconnect()<\/code>.<\/p>\n
\n
observe()<\/code>: The observe method takes in a DOM reference to a target element to be added to the list of elements to be watched by the observer. An observer can have more than one target element, but this method can only accept one target at a time.<\/li>\n
unobserve()<\/code>: The unobserve method takes in a DOM reference to a target element to be removed from the list of elements watched by the observer.<\/li>\n
disconnect()<\/code>: The disconnect method causes the observer to stop watching all of its target elements. The observer itself is still active, but has no targets. After
disconnect()<\/code>, target elements can still be passed to the observer with
observe()<\/code>.<\/li>\n<\/ul>\n
Performance: Intersection Observer versus scroll events<\/h3>\n
requestAnimationFrame<\/a><\/code>.<\/p>\n
Understanding the intersectionRatio property<\/h3>\n
intersectionRatio<\/code> property, given to us by
IntersectionObserverEntry<\/code>, represents the percentage of the target element that is within the boundaries of the root element on an intersection change. I found I didn\u2019t quite understand what this value actually represented at first. For some reason I was thinking it was a straightforward zero to 100 percent representation of the appearance of the target element, which it sort of is. It is tied to the thresholds passed to the observer when it is created. It could be used to determine which threshold was the cause of the intersection change just triggered, as an example. However, the values it provides are not always straightforward.<\/p>\n
\n Intersection Observer: intersectionRatio<\/a> by Travis Almand (@talmand<\/a>)
\n on CodePen<\/a>.<\/span>\n<\/p>\n[...Array(100).keys()].map(x => x \/ 100) }<\/code><\/pre>\n
intersectionRatio<\/code> should then change to 0.5, or something close to that. We now know that half of the target element is intersecting the root element, but we have no idea where it is. More on that later.<\/p>\n
intersectionRatio<\/code> should be somewhere around 0.5. Even though the target element is in a completely different location than before, the resulting ratio is the same.<\/p>\n
intersectionRatio<\/code> should reflect this with a value of around 0.25. Clicking “center” will transform the target element back to the center and fully contained within the root element.<\/p>\n
intersectionRatio<\/code> should be somewhere around 0.8, give or take a few ten-thousandths of a percent. This is the tricky part of relying on
intersectionRatio<\/code>. Creating code based on the thresholds given to the observer makes it possible to have thresholds that will never trigger. In this “large” example, any code based on a threshold of 1 will fail to execute. Also consider situations where the root element can be resized, such as the viewport being rotated from portrait to landscape.<\/p>\n
Finding the position<\/h3>\n
IntersectionObserverEntry<\/code>, so we only have to do simple comparisons.<\/p>\n
\n Intersection Observer: Finding the Position<\/a> by Travis Almand (@talmand<\/a>)
\n on CodePen<\/a>.<\/span>\n<\/p>\nconst output = document.querySelector('#output pre');\r\n\r\nfunction io_callback (entries) {\r\n const ratio = entries[0].intersectionRatio;\r\n const boundingRect = entries[0].boundingClientRect;\r\n const intersectionRect = entries[0].intersectionRect;\r\n\r\n if (ratio === 0) {\r\n output.innerText = 'outside';\r\n } else if (ratio < 1) {\r\n if (boundingRect.top < intersectionRect.top) {\r\n output.innerText = 'on the top';\r\n } else {\r\n output.innerText = 'on the bottom';\r\n }\r\n } else {\r\n output.innerText = 'inside';\r\n }\r\n}<\/code><\/pre>\n
entries[0]<\/code> instead.<\/p>\n
intersectionRect<\/code>\u2019s top, which actually means it\u2019s higher on the page and is seen as “on the top.” In fact, checking against the root element\u2019s “top” would work for this as well. Logically, if the target isn\u2019t at the top, then it must be at the bottom. If the ratio happens to equal one, then it is “inside” the root element. Checking the horizontal position is the same except it’s done with the left or right property.<\/p>\n
if<\/code> check.<\/p>\n
Creating a position sticky event<\/h3>\n
position<\/code> property<\/a> can be a useful feature, yet it\u2019s a bit limiting in terms of CSS and JavaScript. The styling of the sticky element can only be of one design, whether in its normal state or within its sticky state. There\u2019s no easy way to know the state for JavaScript to react to these changes. So far, there\u2019s no pseudo-class or JavaScript event that makes us aware of the changing state of the element.<\/p>\n
\n Intersection Observer: Position Sticky Event<\/a> by Travis Almand (@talmand<\/a>)
\n on CodePen<\/a>.<\/span>\n<\/p>\nrootMargin<\/code> to the observer. The values given are:<\/p>\n
rootMargin: '0px 0px -100% 0px'<\/code><\/pre>\n
isIntersecting<\/code> property of the entry object.<\/p>\n
<section>\r\n <div class=\"sticky-container\">\r\n <div class=\"sticky-content\">\r\n <span>§<\/span>\r\n <h2>Section 1<\/h2>\r\n <\/div>\r\n <\/div>\r\n\r\n {{ content here }}\r\n \r\n<\/section><\/code><\/pre>\n
sticky-container<\/code> is the target for our observer. This div will be set as the sticky element and acts as the container. The element used to style and change the element based on the sticky state is the
sticky-content<\/code> div and its children. This insures that the actual sticky element is always in contact with the shrunken
rootMargin<\/code> at the top of the root element.<\/p>\n
.sticky-content {\r\n position: relative;\r\n transition: 0.25s;\r\n}\r\n\r\n.sticky-content span {\r\n display: inline-block;\r\n font-size: 20px;\r\n opacity: 0;\r\n overflow: hidden;\r\n transition: 0.25s;\r\n width: 0;\r\n}\r\n\r\n.sticky-content h2 {\r\n display: inline-block;\r\n}\r\n \r\n.sticky-container {\r\n position: sticky;\r\n top: 0;\r\n}\r\n\r\n.sticky-container.active .sticky-content {\r\n background-color: rgba(0, 0, 0, 0.8);\r\n color: #fff;\r\n padding: 10px;\r\n}\r\n\r\n.sticky-container.active .sticky-content span {\r\n opacity: 1;\r\n transition: 0.25s 0.5s;\r\n width: 20px;\r\n}<\/code><\/pre>\n
.sticky-container<\/code> creates our sticky element at the top of zero. The rest is a mixture of styles for the regular state in
.sticky-content<\/code> and the sticky state with
.active .sticky-content<\/code>. Again, you can pretty much do anything you want inside the sticky content div. In this demo, there\u2019s a hidden section symbol that appears from a delayed transition when the sticky state is active. This effect would be difficult without something to assist, like the Intersection Observer.<\/p>\n
const stickyContainers = document.querySelectorAll('.sticky-container');\r\nconst io_options = {\r\n root: document.body,\r\n rootMargin: '0px 0px -100% 0px',\r\n threshold: 0\r\n};\r\nconst io_observer = new IntersectionObserver(io_callback, io_options);\r\n\r\nstickyContainers.forEach(element => {\r\n io_observer.observe(element);\r\n});\r\n\r\nfunction io_callback (entries, observer) {\r\n entries.forEach(entry => {\r\n entry.target.classList.toggle('active', entry.isIntersecting);\r\n });\r\n}<\/code><\/pre>\n
rootMargin<\/code>. Take note that this can be repeated for the other three sides as well; it just requires a new observer with its own unique
rootMargin<\/code> with -100% for the appropriate side. There will have to be more unique sticky containers with their own classes such as
sticky-container-top<\/code> and
sticky-container-bottom<\/code>.<\/p>\n
rootMargin<\/code>. This is can be done easily, but if things get resized, not only does the math need to be done again, the observer has to be stopped and restarted with the new value. It\u2019s easier to set the position property to zero and use the interior elements to style things how you want them.<\/p>\n
Combining with Scrolling Events<\/h3>\n
intersectionRatio<\/code> can be imprecise and somewhat limiting. Using scroll events can be more precise but at the cost of inefficiency in performance. What if we combined the two?<\/p>\n
\n Intersection Observer: Scroll Events<\/a> by Travis Almand (@talmand<\/a>)
\n on CodePen<\/a>.<\/span>\n<\/p>\nconst root = document.querySelector('#root');\r\nconst target = document.querySelector('#target');\r\nconst output = document.querySelector('#output pre');\r\nconst io_options = {\r\n root: root,\r\n rootMargin: '0px',\r\n threshold: 0\r\n};\r\nlet io_observer;\r\n\r\nfunction scrollingEvents (e) {\r\n output.innerText = e.timeStamp;\r\n}\r\n\r\nfunction io_callback (entries) {\r\n if (entries[0].isIntersecting) {\r\n root.addEventListener('scroll', scrollingEvents);\r\n } else {\r\n root.removeEventListener('scroll', scrollingEvents);\r\n output.innerText = 0;\r\n }\r\n}\r\n\r\nio_observer = new IntersectionObserver(io_callback, io_options);\r\nio_observer.observe(target);<\/code><\/pre>\n
if-else<\/code> block. The event\u2019s callback function simply updates the div in the output. Whenever the target triggers an intersection change and is not intersecting with the root, we set the output back to zero.<\/p>\n
Interesting differences with browsers<\/h3>\n
Desktop<\/h4>