Change style header/nav with Intersection Observer (IO)

intersection observer not working
intersection observer scrollspy
intersection observer with position sticky
intersection observer position: fixed
css-tricks intersection observer
intersection observer scroll direction
intersection observer top of viewport
nuxt intersection observer

Fiddle latest


I started this question with the scroll event approach, but due to the suggestion of using IntersectionObserver which seems much better approach i'm trying to get it to work in that way.


What is the goal:

I would like to change the style (color+background-color) of the header depending on what current div/section is observed by looking for (i'm thinking of?) its class or data that will override the default header style (black on white).


Header styling:

font-color:

Depending on the content (div/section) the default header should be able to change the font-color into only two possible colors:

  • black
  • white

background-color:

Depending on the content the background-color could have unlimited colors or be transparent, so would be better to address that separate, these are the probably the most used background-colors:

  • white (default)
  • black
  • no color (transparent)

CSS:

header {
  position: fixed;
  width: 100%;
  top: 0;
  line-height: 32px;
  padding: 0 15px;
  z-index: 5;
  color: black; /* default */
  background-color: white; /* default */
}

Div/section example with default header no change on content:

<div class="grid-30-span g-100vh">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_default_header.jpg" 
    class="lazyload"
    alt="">
</div>

Div/section example change header on content:

<div class="grid-30-span g-100vh" data-color="white" data-background="darkblue">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_darkblue.jpg" 
    class="lazyload"
    alt="">
</div>

<div class="grid-30-span g-100vh" data-color="white" data-background="black">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_black.jpg" 
    class="lazyload"
    alt="">
</div>

Intersection Observer approach:

var mq = window.matchMedia( "(min-width: 568px)" );
if (mq.matches) {
  // Add for mobile reset

document.addEventListener("DOMContentLoaded", function(event) { 
  // Add document load callback for leaving script in head
  const header = document.querySelector('header');
  const sections = document.querySelectorAll('div');
  const config = {
    rootMargin: '0px',
    threshold: [0.00, 0.95]
  };

  const observer = new IntersectionObserver(function (entries, self) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        if (entry.intersectionRatio > 0.95) {
          header.style.color = entry.target.dataset.color !== undefined ? entry.target.dataset.color : "black";
          header.style.background = entry.target.dataset.background !== undefined ? entry.target.dataset.background : "white";   
        } else {
        if (entry.target.getBoundingClientRect().top < 0 ) {
          header.style.color = entry.target.dataset.color !== undefined ? entry.target.dataset.color : "black";
          header.style.background = entry.target.dataset.background !== undefined ? entry.target.dataset.background : "white";
          }
        } 
      }
    });
  }, config);

  sections.forEach(section => {
    observer.observe(section);
  });

});

}

Instead of listening to scroll event you should have a look at Intersection Observer (IO). This was designed to solve problems like yours. And it is much more performant than listening to scroll events and then calculating the position yourself.

First, here is a codepen which shows a solution for your problem. I am not the author of this codepen and I would maybe do some things a bit different but it definitely shows you the basic approach on how to solve your problem.

Things I would change: You can see in the example that if you scoll 99% to a new section, the heading changes even tough the new section is not fully visible.

Now with that out of the way, some explaining on how this works (note, I will not blindly copy-paste from codepen, I will also change const to let, but use whatever is more appropriate for your project.

First, you have to specify the options for IO:

let options = {
  rootMargin: '-50px 0px -55%'
}

let observer = new IntersectionObserver(callback, options);

In the example the IO is executing the callback once an element is 50px away from getting into view. I can't recommend some better values from the top of my head but if I would have the time I would try to tweak these parameters to see if I could get better results.

In the codepen they define the callback function inline, I just wrote it that way to make it clearer on what's happening where.

Next step for IO is to define some elements to watch. In your case you should add some class to your divs, like <div class="section">

let entries = document.querySelectorAll('div.section');
entries.forEach(entry => {observer.observe(entry);})

Finally you have to define the callback function:

entries.forEach(entry => {
    if (entry.isIntersecting) {
     //specify what should happen if an element is coming into view, like defined in the options. 
    }
  });

Edit: As I said this is just an example on how to get you started, it's NOT a finished solution for you to copy paste. In the example based on the ID of the section that get's visible the current element is getting highlighted. You have to change this part so that instead of setting the active class to, for example, third element you set the color and background-color depending on some attribute you set on the Element. I would recommend using data attributes for that.

Edit 2: Of course you can continue using just scroll events, the official Polyfill from W3C uses scroll events to emulate IO for older browsers.it's just that listening for scroll event and calculating position is not performant, especially if there are multiple elements. So if you care about user experience I really recommend using IO. Just wanted to add this answer to show what the modern solution for such a problem would be.

Edit 3: I took my time to create an example based on IO, this should get you started.

Basically I defined two thresholds: One for 20 and one for 90%. If the element is 90% in the viewport then it's save to assume it will cover the header. So I set the class for the header to the element that is 90% in view.

Second threshold is for 20%, here we have to check if the element comes from the top or from the bottom into view. If it's visible 20% from the top then it will overlap with the header.

Adjust these values and adapt the logic as you see.

const sections = document.querySelectorAll('div');
const config = {
  rootMargin: '0px',
  threshold: [.2, .9]
};

const observer = new IntersectionObserver(function (entries, self) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      var headerEl = document.querySelector('header');
      if (entry.intersectionRatio > 0.9) {
        //intersection ratio bigger than 90%
        //-> set header according to target
        headerEl.className=entry.target.dataset.header;      
      } else {
        //-> check if element is coming from top or from bottom into view
        if (entry.target.getBoundingClientRect().top < 0 ) {
          headerEl.className=entry.target.dataset.header;
        }
      } 
    }
  });
}, config);

sections.forEach(section => {
  observer.observe(section);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.g-100vh {
height: 100vh
}

header {
  min-height: 50px;
  position: fixed;
  background-color: green;
  width: 100%;
}
  
header.white-menu {
  color: white;
  background-color: black;
}

header.black-menu {
  color: black;
  background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>


<header>
 <p>Header Content </p>
</header>
<div class="grid-30-span g-100vh white-menu" style="background-color:darkblue;" data-header="white-menu">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_darkblue.jpg" 
    class="lazyload"
    alt="<?php echo $title; ?>">
</div>

<div class="grid-30-span g-100vh black-menu" style="background-color:lightgrey;" data-header="black-menu">
    <img 
    src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1.414 1'%3E%3C/svg%3E"
    data-src="/images/example_lightgrey.jpg" 
    class="lazyload"
    alt="<?php echo $title; ?>">
</div>

Change style header/nav with Intersection Observer (IO), I would like to change the style ( color + background-color ) of the header depending on what current div / section is observed by looking for (i'm  In cases where an observer contains multiple targets, this is the easy way to determine which target element triggered this intersection change. The time property provides the time (in milliseconds) from when the observer is first created to the time this intersection change is triggered.

I might not understand the question completely, but as for your example - you can solve it by using the mix-blend-mode css property without using javascript at all.

Example:

header {background: white; position: relative; height: 20vh;}
header h1 {
  position: fixed;
  color: white;
  mix-blend-mode: difference;
}
div {height: 100vh; }
<header>
  <h1>StudioX, Project Title, Category...</h1>
</header>
<div style="background-color:darkblue;"></div>
<div style="background-color:lightgrey;"></div>

How to change your navigation style on scroll, This video explores using the Intersection Observer API to watch for an element leaving the Duration: 13:22 Posted: 19 Jun 2019 Users are also able to switch between content sections with a smooth scroll effect by clicking the items in the navigation. Heavily based on the Intersection Observer API. How to use it: 1. Create a list of nav links pointing to the page sections within the document.

This still needs adjustment, but you could try the following:

const header = document.getElementsByTagName('header')[0];

const observer = new IntersectionObserver((entries) => {
	entries.forEach((entry) => {
    if (entry.isIntersecting) {
    	  header.style.color = entry.target.dataset.color || '';
        header.style.backgroundColor = entry.target.dataset.background;
    }
  });
}, { threshold: 0.51 });

[...document.getElementsByClassName('observed')].forEach((t) => {
    t.dataset.background = t.dataset.background || window.getComputedStyle(t).backgroundColor;
    observer.observe(t);    
});
body {
  font-family: arial;
  margin: 0;
}

header {
  border-bottom: 1px solid red;
  margin: 0 auto;
  width: 100vw;
  display: flex;
  justify-content: center;
  position: fixed;
  background: transparent;  
  transition: all 0.5s ease-out;
}

header div {
  padding: 0.5rem 1rem;
  border: 1px solid red;
  margin: -1px -1px -1px 0;
}

.observed {
  height: 100vh;
  border: 1px solid black;
}

.observed:nth-of-type(2) {
  background-color: grey;
}

.observed:nth-of-type(3) {
  background-color: white;
}
<header>
  <div>One</div>
  <div>Two</div>
  <div>Three</div>
</header>

<div class="observed">
  <img src="http://placekitten.com/g/200/300">
  <img src="http://placekitten.com/g/400/300">
</div>
  
<div class="observed" data-color="white" data-background="black">
  <img src="http://placekitten.com/g/600/300">
</div>

<div class="observed" data-color="black" data-background="white">
  <img src="http://placekitten.com/g/600/250">
</div>

Intersection observer & position: sticky, <h2>The event contains an 'isIntersecting' value - which tells us if the pink bar is/​isn't in the viewport.</h2>. 24. </div> ! CSS (SCSS). CSS (SCSS). CSS Options. Using Intersection Observer makes it less resource intensive and a lot easier to implement compared to listening for scroll events and checking if an element is about to enter the viewport. Behind the scenes, the Intersection Observer API makes use of requestIdleCallback to help with performance even more.

Master the Intersection Observer API, Intersection Observer provides a fantastic alternative to traditional, elements and monitor changes in their intersection with a given ancestor element, or the viewport itself. The final part of the navigation element is to style the links themselves. The easiest way to use the Polyfill is to use polyfill.io. In this article, we'll build a mock blog which has a number of ads interspersed among the contents of the page, then use the Intersection Observer API to track how much time each ad is visible to the user. When an ad exceeds one minute of visible time, it will be replaced with a new one.

Intersection Observer: Track Elements Scrolling Into View, The “Intersection Observer” provides a way to asynchronously observe changes in the intersection (overlapping) of elements. This can be You can always make sure your intended browsers/devices support the IO API by referencing caniuse. These values passed are similar to the CSS margin property. It stays on the screen when it can, but won’t overlap the header, footer, or ever make any of it’s links inaccessible. Scrolls smoothly to the sections you click to. Activates the current nav based on scroll position (it’s a single page thing). See the Pen Sticky, Smooth, Active Nav by Chris Coyier (@chriscoyier) on CodePen. Sticky

An event for CSS position:sticky | Web, Using position: sticky and IntersectionObserver together to determine when elements Speed up Service Worker with Navigation Preloads · Lighthouse January The demo uses this event to headers a drop shadow when they become fixed. the container, their visibility changes and Intersection Observer fires a callback. 7 Change style header/nav with Intersection Observer (IO) Sep 9 '19 4 how to extract attribute value using javascript Sep 11 '19 4 Javascript cancel custom function and resume Aug 12 '19

Comments
  • Your question isn't clear enough. When entering a page and content (div) has header class added it only changes just after scrolling, but it should change the header directly on entering the page. What color do you expect the header background and text to be when the page loads? When do you expect it to change, and to what color?
  • With absolutely none class determined the background of default header turns or stays transparent after scrolling. I don't understand what that means at all. Please specify the expected behavior and how it differs from the current behavior (especially the colors, since that seems to be the main issue here).
  • When you open the project-page you see content (mostly images set to 100% width and in 100vh height) in some occasions i want to add a class that suits the content so the header/nav stays visible, but with the code i have now it changes the header/nav into the class only after scrolling
  • It needs to read the class when opening the page knowing to change the header, after that the scrolling part takes place so when leaving that content it returns to the default header setting black on white background not black on transparent what happens now.
  • In the fiddle you see an example: the second section (section-grey) that has no class determined but those not return completely to the default header css setting
  • First of all thank you for taking the time to help, but the codepen example you just showed me isn't really what i'm after, that works with nav tabs selections i don't need that. Just header / nav to change on class when scrolling and listen to the class when opening page if content has class added. And show default header when no class is added at all.
  • You don't have to use nav tabs, you can also just change the whole navigation bar. In the codepen it checks for active class and depending on which element is visible it highlights the current section. You can just skip this part and change the color depending on the section that get's visible. I will edit this part into my answer.
  • It does not work in my case, all of the navigation what is placed in my header has his color and background determined by just one 'class' (header). In your approach when content is for example dark and i need the header / nav to be white on transparent background it isn't possible, because it listens to data-ref set for each list-item seems to be comprehensive for what i need.
  • It totally is possible, you can adjust the logic according to your specific needs. You know which element is getting into view and you can react to it however you want. Check it's classname or some other attributes, whatever you need and set your colors accordingly. IO also solves your problem on setting the correct colors on page load if it's already scrolled.
  • When i use the data-ref="white-menu" on the content and set id to the whole header / nav instead of each list-item i probably can only use one class (id in this case) to change its appearance, because when set it to data-ref="black-menu"` when i need black on transparent depending on what content it doesn't listen to that because the header already has an id that only listens to white-menu
  • Thank you for answering this still pending question. I just tried your code, but it doesn't work. Also here in the example it doesn't return to the default header when scrolling back. The approach seems the way i had explained, to address the color and background separate
  • It not returning to its default state is due to the times it fires - whenever one of the targets comes into view by at least 50% (the threshold). Which means the last time it fired before scrolling back was on the second div. If you insist on using an intersectionobserver, you need to fiddle around with the config, BUT I suspect a listener on scroll, as you intended initially, would be more suited to your goal as a fixed header is always intersecting.
  • After the user: cloned told me about IO got more information about it and it is a more modern approach than the old listener on scroll function which as his downfalls, but even if it was it didn't work exactly as i wanted. Fixed header does not have any issues of intersecting al the time, that isn't true or the case. There lots of examples of changing a fixed header on scroll using intersection observer: youtu.be/RxnV9Xcw914
  • As I've understood your goal, you want to change the styling of your header when the user has scrolled down. The problem I tried to point out is with choosing the target: the document-root is always intersecting and any other element will have finicky behavior as in my example. You might try to add an invisible alias-element beneath the header, but I think it would be much easier to watch the yOffset. The performance issues should be negligible on a small project like a portfolio.
  • It isn't always the case of changing on scroll sometimes the header keeps his default state. So for example the project has a blue background i would like to add white-menu and background-color="blue" or keep it transparent it depends on the project content. So when scrolling down the content (images of video) each of those will tell the header what it should be when intersecting or do nothing and keep the default header.