The Intersection Observer API is a Javascript API that allows us to look at a component and detect when it passes a specified level in a scrolling container — typically (however not at all times) the viewport — triggering a callback perform.
Intersection Observer could be thought-about extra performant than listening for scroll occasions on the primary thread, as it’s asynchronous, and the callback will solely fireplace when the component we’re observing meets the required threshold, as an alternative each time the scroll place is up to date. On this article, we’ll stroll via an instance of how we will use Intersection Observer to construct a set header part that adjustments when it intersects with completely different sections of the webpage.
Fundamental Utilization
To make use of Intersection Observer, we have to first create a brand new observer, which takes two parameters: An object with the observer’s choices, and the callback perform that we need to execute each time the component we’re observing (referred to as the observer goal) intersects with the foundation (the scrolling container, which should be an ancestor of the goal component).
const choices = {
root: doc.querySelector(‘[data-scroll-root]’),
rootMargin: ‘0px’,
threshold: 1.0
}
const callback = (entries, observer) => {
entries.forEach((entry) => console.log(entry))
}
const observer = new IntersectionObserver(callback, choices)
After we’ve created our observer, we then have to instruct it to look at a goal component:
const targetEl = doc.querySelector(‘[data-target]’)
observer.observe(targetEl)
Any of the choices values could be omitted, as they’ll fall again to their default values:
const choices = {
rootMargin: ‘0px’,
threshold: 1.0
}
If no root is specified, then it will likely be classed because the browser viewport. The above code instance exhibits the default values for each rootMargin and threshold. These could be arduous to visualise, so are price explaining:
rootMargin
The rootMargin worth is a bit like including CSS margins to the foundation component — and, identical to margins, can take a number of values, together with unfavourable values. The goal component might be thought-about to be intersecting relative to the margins.
That signifies that a component can technically be classed as “intersecting” even when it’s out of view (if our scroll root is the viewport).
rootMargin defaults to 0px, however can take a string consisting of a number of values, identical to utilizing the margin property in CSS.
threshold
The brink can encompass a single worth or an array of values between 0 and 1. It represents the proportion of the component that should be inside the root bounds for it to be thought-about intersecting. Utilizing the default worth of 1, the callback will fireplace when 100% of the goal component is seen inside the root.
It’s not at all times simple to visualise when a component might be classed as seen utilizing these choices. I’ve constructed a small software to assist familiarize yourself with Intersection Observer.
Creating The Header
Now that we’ve grasped the fundamental rules, let’s begin constructing our dynamic header. We’ll begin with a webpage divided up into sections. This picture exhibits the whole format of the web page we’ll be constructing:
I’ve included a demo on the finish of this text, so be happy to soar straight to it in the event you’re eager to unpick the code. (There’s additionally a Github repository.)
Every part has a minimal top of 100vh (though they could possibly be longer, relying on content material). Our header is mounted on the high of the web page and stays in place because the consumer scrolls (utilizing place: mounted). The sections have completely different coloured backgrounds, and once they meet the header, the colours of the header change to enhance these of the part. There may be additionally a marker to indicate the present part the consumer is in, which slides alongside when the following part arrives.
To make it simpler for us to get straight to the related code, I’ve arrange a minimal demo with our place to begin (earlier than we begin utilizing the Intersection Observer API), in case you’d wish to observe alongside.
Markup
We’ll begin with the HTML for our header. That is going to be a reasonably easy header with a house hyperlink and navigation, nothing particularly fancy, however we’re going to make use of a few knowledge attributes: data-header for the header itself (so we will goal the component with JS), and three anchor hyperlinks with the attribute data-link, which can scroll the consumer to the related part when clicked:
<header data-header>
<nav class=”header__nav”>
<div class=”header__left-content”>
<a href=”#0″>House</a>
</div>
<ul class=”header__list”>
<li>
<a href=”#about-us” data-link>About us</a>
</li>
<li>
<a href=”#flavours” data-link>The flavours</a>
</li>
<li>
<a href=”#get-in-touch” data-link>Get in contact</a>
</li>
</ul>
</nav>
</header>
Subsequent, the HTML for the remainder of our web page, which is split up into sections. For brevity, I’ve solely included the components related to the article, however the full markup is included within the demo. Every part features a knowledge attribute specifying the identify of the background coloration, and an id that corresponds to one of many anchor hyperlinks within the header:
<essential>
<part data-section=”raspberry” id=”dwelling”>
<!–Part content–>
</part>
<part data-section=”mint” id=”about-us”>
<!–Part content–>
</part>
<part data-section=”vanilla” id=”the-flavours”>
<!–Part content–>
</part>
<part data-section=”chocolate” id=”get-in-touch”>
<!–Part content–>
</part>
</essential>
We’ll place our header with CSS so that it’s going to keep mounted on the high of the web page because the consumer scrolls:
header {
place: mounted;
width: 100%;
}
We’ll additionally give our sections a minimal top, and middle the content material. (This code isn’t mandatory for the Intersection Observer to work, it’s only for the design.)
part {
padding: 5rem 0;
min-height: 100vh;
show: flex;
justify-content: middle;
align-items: middle;
}
iframe Warning
Whereas constructing this Codepen demo, I ran right into a perplexing difficulty the place my Intersection Observer code that ought to have labored completely was failing to fireside the callback on the appropriate level of the intersection however as an alternative firing when the goal component intersected with the viewport edge. After a little bit of head-scratching, I spotted that this was as a result of in Codepen the content material is loaded inside an iframe, which is handled in another way. (See the part of the MDN docs on Clipping and the intersection rectangle for full particulars.)
As a workaround, within the demo we will wrap our markup in one other component, which can act because the scrolling container — the foundation in our IO choices — reasonably than the browser viewport, as we’d anticipate:
<div class=”scroller” data-scroller>
<header data-header>
<!–Header content–>
</header>
<essential>
<!–Sections–>
</essential>
</div>
If you wish to see find out how to use the viewport as the foundation as an alternative for a similar demo, that is included within the Github repository.
CSS
In our CSS we’ll outline some customized properties for the colours we’re utilizing. We’ll additionally outline two extra customized properties for the header textual content and background colours, and set some preliminary values. (We’re going to replace these two customized properties for the completely different sections in a while.)
:root {
–mint: #5ae8d5;
–chocolate: #573e31;
–raspberry: #f2308e;
–vanilla: #faf2c8;
–headerText: var(–vanilla);
–headerBg: var(–raspberry);
}
We’ll use these customized properties in our header:
header {
background-color: var(–headerBg);
coloration: var(–headerText);
}
We’ll additionally set the colours for our completely different sections. I’m utilizing the info attributes because the selectors, however you possibly can simply as simply use a category in the event you favor.
[data-section=”raspberry”] {background-color: var(–raspberry);
coloration: var(–vanilla);
} [data-section=”mint”] {
background-color: var(–mint);
coloration: var(–chocolate);
} [data-section=”vanilla”] {
background-color: var(–vanilla);
coloration: var(–chocolate);
} [data-section=”chocolate”] {
background-color: var(–chocolate);
coloration: var(–vanilla);
}
We will additionally set some types for our header when every part is in view:
/* Header */
[data-theme=”raspberry”] {
–headerText: var(–raspberry);
–headerBg: var(–vanilla);
}
–headerText: var(–mint);
–headerBg: var(–chocolate);
} [data-theme=”chocolate”] {
–headerText: var(–chocolate);
–headerBg: var(–vanilla);
}
There’s a stronger case for utilizing knowledge attributes right here as a result of we’re going to toggle the data-theme attribute of the header upon every intersection.
Creating The Observer
Now that we have now the fundamental HTML and CSS for our web page arrange, we will create an observer to look at for every of our sections coming into view. We need to fireplace a callback each time a piece comes into contact with the underside of the header as we’re scrolling down the web page. This implies we have to set a unfavourable root margin that corresponds to the peak of the header.
const header = doc.querySelector(‘[data-header]’)
const sections = […document.querySelectorAll(‘[data-section]’)]
const scrollRoot = doc.querySelector(‘[data-scroller]’)
const choices = {
root: scrollRoot,
rootMargin: `${header.offsetHeight * -1}px`,
threshold: 0
}
We’re setting a threshold of 0, as we wish it to fireside if any a part of the part is intersecting with the foundation margin.
Initially, we’ll create a callback to vary the data-theme worth of the header. (That is extra simple than including and eradicating courses, particularly when our header component could produce other courses utilized.)
/* The callback that may fireplace on intersection */
const onIntersect = (entries) => {
entries.forEach((entry) => {
const theme = entry.goal.dataset.part
header.setAttribute(‘data-theme’, theme)
})
}
Then we’ll create the observer to look at for the sections intersecting:
/* Create the observer */
const observer = new IntersectionObserver(onIntersect, choices)
/* Set our observer to look at every part */
sections.forEach((part) => {
observer.observe(part)
})
Now we should always see our header colours replace when every part meets the header.
See the Pen Comfortable Face Ice Cream Parlour – Step 2 by Michelle Barker.
Nonetheless, you may discover that the colours aren’t updating accurately as we scroll down. In actual fact, the header is updating with the earlier part’s colours every time! Scrolling upwards, alternatively, it really works completely. We have to decide the scroll course and alter the conduct accordingly.
Discovering The Scroll Route
We’ll set a variable in our JS for the course of scroll, with an preliminary worth of ‘up’, and one other for the final recognized scroll place (prevYPosition). Then, inside the callback, if the scroll place is bigger than the earlier worth, we will set the course worth as ‘down’, or ‘up’ if vice versa.
let course = ‘up’
let prevYPosition = 0
const setScrollDirection = () => {
if (scrollRoot.scrollTop > prevYPosition) {
course = ‘down’
} else {
course = ‘up’
}
prevYPosition = scrollRoot.scrollTop
}
const onIntersect = (entries, observer) => {
entries.forEach((entry) => {
setScrollDirection()
/* … */
})
}
We’ll additionally create a brand new perform to replace the header colours, passing within the goal part as an argument:
const updateColors = (goal) => {
const theme = goal.dataset.part
header.setAttribute(‘data-theme’, theme)
}
const onIntersect = (entries) => {
entries.forEach((entry) => {
setScrollDirection()
updateColors(entry.goal)
})
}
Up to now we should always see no change to the conduct of our header. However now that we all know the scroll course, we will go in a special goal for our updateColors() perform. If the scroll course is up, we’ll use the entry goal. If it’s down, we’ll use the following part (if there may be one).
const getTargetSection = (goal) => {
if (course === ‘up’) return goal
if (goal.nextElementSibling) {
return goal.nextElementSibling
} else {
return goal
}
}
const onIntersect = (entries) => {
entries.forEach((entry) => {
setScrollDirection()
const goal = getTargetSection(entry.goal)
updateColors(goal)
})
}
There’s yet another difficulty, nevertheless: the header will replace not solely when the part hits the header, however when the following component comes into view on the backside of the viewport. It is because our observer fires the callback twice: as soon as because the component is coming into, and once more because it’s leaving.
To find out whether or not the header ought to replace, we will use the isIntersecting key from the entry object. Let’s create one other perform to return a boolean worth for whether or not the header colours ought to replace:
const shouldUpdate = (entry) => {
if (course === ‘down’ && !entry.isIntersecting) {
return true
}
if (course === ‘up’ && entry.isIntersecting) {
return true
}
return false
}
We’ll replace our onIntersect() perform accordingly:
const onIntersect = (entries) => {
entries.forEach((entry) => {
setScrollDirection()
/* Do nothing if no have to replace */
if (!shouldUpdate(entry)) return
const goal = getTargetSection(entry.goal)
updateColors(goal)
})
}
Now our colours ought to replace accurately. We will set a CSS transition, in order that the impact is just a little nicer:
header {
transition: background-color 200ms, coloration 200ms;
}
See the Pen Comfortable Face Ice Cream Parlour – Step 3 by Michelle Barker.
Including The Dynamic Marker
Subsequent we’ll add a marker to the header that updates its place as we scroll to the completely different sections. We will use a pseudo-element for this, so we don’t want so as to add something to our HTML. We’ll give it some easy CSS styling to place it on the high left of the header, and provides it a background coloration. We’re utilizing currentColor for this, as it is going to tackle the worth of the header textual content coloration:
header::after {
content material: ”;
place: absolute;
high: 0;
left: 0;
top: 0.4rem;
background-color: currentColor;
}
We will use a customized property for the width, with a default worth of 0. We’ll additionally use a customized property for the translate x worth. We’re going to set the values for these in our callback perform because the consumer scrolls.
header::after {
content material: ”;
place: absolute;
high: 0;
left: 0;
top: 0.4rem;
width: var(–markerWidth, 0);
background-color: currentColor;
remodel: translate3d(var(–markerLeft, 0), 0, 0);
}
Now we will write a perform that may replace the width and place of the marker on the level of intersection:
const updateMarker = (goal) => {
const id = goal.id
/* Do nothing if no goal ID */
if (!id) return
/* Discover the corresponding nav hyperlink, or use the primary one */
let hyperlink = headerLinks.discover((el) => {
return el.getAttribute(‘href’) === `#${id}`
})
hyperlink = hyperlink || headerLinks[0]
/* Get the values and set the customized properties */
const distanceFromLeft = hyperlink.getBoundingClientRect().left
header.fashion.setProperty(‘–markerWidth’, `${hyperlink.clientWidth}px`)
header.fashion.setProperty(‘–markerLeft’, `${distanceFromLeft}px`)
}
We will name the perform on the identical time we replace the colours:
const onIntersect = (entries) => {
entries.forEach((entry) => {
setScrollDirection()
if (!shouldUpdate(entry)) return
const goal = getTargetSection(entry.goal)
updateColors(goal)
updateMarker(goal)
})
}
We’ll additionally have to set an preliminary place for the marker, so it doesn’t simply seem out of nowhere. When the doc is loaded, we’ll name the updateMarker() perform, utilizing the primary part because the goal:
doc.addEventListener(‘readystatechange’, e => {
if (e.goal.readyState === ‘full’) {
updateMarker(sections[0])
}
})
Lastly, let’s add a CSS transition in order that the marker slides throughout the header from one hyperlink to the following. As we’re transitioning the width property, we will use will-change to allow the browser to carry out optimizations.
header::after {
transition: remodel 250ms, width 200ms, background-color 200ms;
will-change: width;
}
Easy Scrolling
For a remaining contact, it might be good if, when a consumer clicks a hyperlink, they’re scrolled easily down the web page, as an alternative of it leaping to the part. Lately we will do it proper in our CSS, no JS required! For a extra accessible expertise, it’s a good suggestion to respect the consumer’s movement preferences by solely implementing clean scrolling in the event that they haven’t specified a desire for diminished movement of their system settings:
@media (prefers-reduced-motion: no-preference) {
.scroller {
scroll-behavior: clean;
}
}
Ultimate Demo
Placing all of the above steps collectively ends in the whole demo.
See the Pen Comfortable Face Ice Cream Parlour – Intersection Observer instance by Michelle Barker.
Browser Help
Intersection Observer is extensively supported in fashionable browsers. The place mandatory it may be polyfilled for older browsers — however I favor to take a progressive enhancement strategy the place potential. Within the case of our header, it might not be vastly detrimental to the consumer expertise to supply a easy, unchanging model for non-supporting browsers.
To detect if Intersection Observer is supported, we will use the next:
/ Code to execute if IO is supported /
} else {
/ Code to execute if not supported /
}
Sources
Learn extra about Intersection Observer:
Intensive documentation, with some sensible examples from MDN
Intersection Observer visualiser software
Timing Aspect Visibility with the Intersection Observer API – one other tutorial from MDN, that appears at how IO can be utilized to trace advert visibility
This text by Denys Mishunov covers another makes use of for IO, together with lazy-loading belongings. Though that’s much less mandatory now (due to the loading attribute), there’s nonetheless lots to study right here.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!