{"id":4990,"date":"2025-01-20T12:21:36","date_gmt":"2025-01-20T17:21:36","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=4990"},"modified":"2025-01-20T12:21:38","modified_gmt":"2025-01-20T17:21:38","slug":"simplify-lazy-loading-with-intersection-observers-scrollmargin","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/simplify-lazy-loading-with-intersection-observers-scrollmargin\/","title":{"rendered":"Simplify Lazy Loading With Intersection Observer\u2019s ScrollMargin"},"content":{"rendered":"\n<p>The<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Intersection_Observer_API\"> Intersection Observer API<\/a> has since December 2023 in Chrome and Edge 120 been shipped with the new <code>options<\/code> property <code>scrollMargin<\/code>. However,<strong> its convenience seems to be understated.<\/strong> Even the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/IntersectionObserver\/IntersectionObserver\">MDN documentation on the Intersection Observer constructor <\/a>has yet to be updated with information about this property. It is definitely an upgrade to the Intersection Observer API, so I want to tell you more about it, particularly because it has already helped with with projects at work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Problem with Intersection Observer\u2019s <code>rootMargin<\/code><\/h2>\n\n\n\n<p>The Intersection Observer API is a JavaScript API that makes it possible to know whether a DOM element is in the viewport or not. An intersection observers constructor takes an options object, in which a <code>rootMargin<\/code> can be specified. Its value is <em>\u201ca string which specifies a set of offsets to add to the root&#8217;s bounding box when calculating intersections, effectively shrinking or growing the root <\/em>[&#8230;]<em> The syntax is approximately the same as that for the CSS margin property\u201d <\/em>(<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/IntersectionObserver\/IntersectionObserver#rootmargin\">MDN<\/a>)<em>.<\/em><\/p>\n\n\n\n<p>To give you an example: if you want some code to run <em>before<\/em> the element is actually in view, <code>rootMargin: \"25%\"<\/code> will make the Intersection Observer report an intersection when the observed element is 25% away from the viewport.<\/p>\n\n\n\n<p>However, <strong>a problem arises<\/strong> if the Intersection Observer:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>uses the default root (the document\u2019s viewport)<\/li>\n\n\n\n<li>is specified with a <code>rootMargin<\/code><\/li>\n\n\n\n<li>observes an element inside a scroll container<\/li>\n<\/ol>\n\n\n\n<p>In that case, the intersection in the scroll direction is reported <strong>as if no <code>rootMargin<\/code> has been specified. <\/strong>This makes it impossible to get an intersection <em>before<\/em> the element in a scroll container is actually visible, which is a problem when trying to lazy load thing <em>just <strong>before<\/strong><\/em> they are visible. <\/p>\n\n\n\n<p>The workaround is to use the scroll container as the root instead of Intersection Observer\u2019s default root. But it would be convenient if it was possible to keep using the default root!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><code>scrollMargin<\/code> is the New Solution<\/h2>\n\n\n\n<p>The new Intersection Observer options property <code>scrollMargin<\/code> aims to rectify this. Without it, when the root is the document viewport, the scroll containers are clipped away. This is the cause of the aforementioned problem. With the new Intersection Observer property, each scroll container is expanded by the <code>scrollMargin<\/code> when calculating the intersection.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Typical Use Case<\/h2>\n\n\n\n<p>The use case for the <code>scrollMargin<\/code> property is using it as an indicator to request data when it is <em>about<\/em> to be scrolled into view. That&#8217;s called lazy loading, the point of which is to <em>not<\/em> load data that is so far off screen it&#8217;s possible the user never scrolls to see it.<\/p>\n\n\n\n<p>The following Pen lazy loads data <em>both<\/em> in the vertical and horizontal directions. The UI\/UX is like a streaming service, which is exactly where my use case and thus learning of this feature comes from. Similar use cases are scrolling carousels for eCommerce, news, images, etc.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_zxOPWMZ\" src=\"\/\/codepen.io\/anon\/embed\/zxOPWMZ?height=450&amp;theme-id=47434&amp;slug-hash=zxOPWMZ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed zxOPWMZ\" title=\"CodePen Embed zxOPWMZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Video Examples<\/h2>\n\n\n\n<p>This is what lazy loading looks like <em>without<\/em> <code>scrollMargin<\/code> in place. Notice how you can see the element before the data is ready, and there is a noticable loading delay.<\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='337' src='https:\/\/videopress.com\/embed\/8Da5aNMP?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1725245713'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>Here&#8217;s a video example of when we have <code>scrollMargin<\/code> in place in a supporting browser. Notice that the data is loaded ahead of time and it&#8217;s ready to go when the user gets there. <\/p>\n\n\n\n\t\t<figure class=\"wp-block-jetpack-videopress jetpack-videopress-player\" style=\"\" >\n\t\t\t<div class=\"jetpack-videopress-player__wrapper\"> <iframe title=\"VideoPress Video Player\" aria-label='VideoPress Video Player' width='500' height='336' src='https:\/\/videopress.com\/embed\/OaPINqKl?cover=1&amp;autoPlay=0&amp;controls=1&amp;loop=0&amp;muted=0&amp;persistVolume=1&amp;playsinline=0&amp;preloadContent=metadata&amp;useAverageColor=1&amp;hd=0' frameborder='0' allowfullscreen data-resize-to-parent=\"true\" allow='clipboard-write'><\/iframe><script src='https:\/\/v0.wordpress.com\/js\/next\/videopress-iframe.js?m=1725245713'><\/script><\/div>\n\t\t\t\n\t\t\t\n\t\t<\/figure>\n\t\t\n\n\n<p>In the video above it looks like we&#8217;re just horizontally scrolling some elements that are already loaded, but rest assured, they are being loaded <em>just in time<\/em> and are still properly lazy loading.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Browser Support<\/h2>\n\n\n\n<p>At the time of writing (January 2025), only Chrome and Edge since version 120 support this new property. The status of the feature can be followed on the <a href=\"https:\/\/chromestatus.com\/feature\/5091020593430528\">Chrome Platform Status Feature: Intersection Observer Scroll Margin page<\/a>. It is already a part of the <a href=\"https:\/\/w3c.github.io\/IntersectionObserver\/#dom-intersectionobserver-scrollmargin\">W3C\u2019s Intersection Observer specification<\/a>. When it comes to Safari, it has been reported as <a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=264864\">a missing feature on the Webkit Bugzilla platform<\/a> since Nov 2023.<\/p>\n\n\n\n<p>Unfortunately, I will caution against using <code>scrollMargin<\/code> as a progressive enhancement. The consequence is that in Safari, the data will not load before it is scrolled into view, and the user will see the data appearing. This sounds innocent enough, but there is a problem with VoiceOver in Safari. It&#8217;s a \u201cvicious circle\u201d type of problem: in order for the screen reader to navigate to the element, it needed to have its data loaded, but in order to load its data, it needed to be scrolled into view.<\/p>\n\n\n\n<p>In spite of the lacking Safari support, I wanted to write about this property. The reason is that I think we as web developers should all push for <a href=\"https:\/\/wpt.fyi\/interop-2024\">web browser interoperability<\/a>, and from my experience, I think the web platform would benefit from having <code>scrollMargin<\/code> supported.<\/p>\n\n\n\n<p><em>Thanks to Johannes Odland.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This helps load in data just *before* a user gets to it, and it works with non-root containers and horizontal scrolling.<\/p>\n","protected":false},"author":34,"featured_media":5004,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[286,3],"class_list":["post-4990","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-intersectionobserver","tag-javascript"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/01\/scrollMargin.png?fit=1588%2C1046&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4990","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/34"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=4990"}],"version-history":[{"count":6,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4990\/revisions"}],"predecessor-version":[{"id":5006,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/4990\/revisions\/5006"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/5004"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=4990"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=4990"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=4990"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}