javascripthtmlcsscss-animationsintersection-observer

Laggy/Glitchy CSS Animations only on Firefox with Intersection Observer


I'm utilizing Intersection Observer API to hide the website header logo when it's not over (or close to being over) a background image.

Works well on Google Chrome, other chromium browsers and Safari (at the least mobile version) but the CSS animations play extremely laggy and sometimes delayed on Firefox. In fact sometimes it doesn't even trigger, it just freezes.

The CSS animations display fine when it's not triggered by Intersection Observer adding a class. (i.e.: the blue box in the example below has the fade-in class by default and the animation plays smoothly when you refresh the page and it fades in.)

I'm using Firefox 132.0.1 (64-bit). I've asked others to test the website and the results were the same across.

Here is a sample page I made replicating the issue with a blue square as the header logo and red rectangle as the background image region:

const header = document.getElementById("headerhide");
const region = document.querySelector(".header-background");

const observer = new IntersectionObserver(([entry]) => {
  header.classList.toggle("fade-out", !entry.isIntersecting);
  header.classList.toggle("fade-in", entry.isIntersecting);

}, {
  threshold: 0.2,
})
observer.observe(region)
.header-background {
  height: 500px;
  background-color: red;
}

.logo-container {
  position: fixed;
  top: 2%;
  left: 50%;
  transform: translate(-50%, 0);
  width: 80px;
  height: 80px;
  background-color: blue;
}

.logo.logo-container {
  position: absolute;
}

.container {
  height: 2000px;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

.fade-in {
  opacity: 0;
  animation-name: fade-in;
  animation-duration: 0.4s;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}

.fade-out {
  opacity: 1;
  animation-name: fade-out;
  animation-duration: 0.2s;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}
<div class="container">
  <div class="headerhide" id="headerhide">
    <div class="logo-container">
      <div class="logo"></div>
    </div>
  </div>
  <div class="header-background" id="region"></div>
</div>

(edit: also on JSFiddle: https://jsfiddle.net/ob2t6wn3/3/)

(the CSS and js is normally in different files but for the example's sake I put them all in the html)

My specs (although I've tested on different devices and the issue persisted.)

OS: Windows 11 Home [64-bit] CPU: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz GPU: NVIDIA GeForce RTX 2070 Memory: 16GB Motherboard: Micro-Star International Co., Ltd. MS-

Things I've tried:


Solution

  • I've managed to make it work on my end using the code below.

    I remove the positioning-values (top, position: fixed etc.) and applied them to the element being observed instead - #headerhide.

    I cannot give you a thorough explanation as to why this works, but I'm assuming it's either because #headerhide before served as an empty container with a height of 0, because its only child element had position: fixed, or because the animation now runs directly on the element that is visible.

    But the above is just guesswork.

    const header = document.getElementById("headerhide");
    const region = document.querySelector(".header-background");
    
    const observer = new IntersectionObserver(([entry]) => {
      header.classList.toggle("fade-out", !entry.isIntersecting);
      header.classList.toggle("fade-in", entry.isIntersecting);
    
    }, {
      threshold: 0.2,
    })
    observer.observe(region)
    .header-background {
      height: 500px;
      background-color: red;
    }
    
    #headerhide {
      width: 80px;
      height: 80px;
      z-index: 100;
      top: 2%;
      left: 50%;
      transform: translate(-50%, 0);
      position: fixed;
    }
    
    .logo-container {
      background-color: blue;
      height: 100%;
      width: 100%;
    }
    
    .logo.logo-container {
      position: absolute;
    }
    
    .container {
      height: 2000px;
    }
    
    @keyframes fade-in {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }
    
    @keyframes fade-out {
      from {
        opacity: 1;
      }
      to {
        opacity: 0;
      }
    }
    
    .fade-in {
      opacity: 0;
      animation-name: fade-in;
      animation-duration: 0.4s;
      animation-timing-function: ease-out;
      animation-fill-mode: forwards;
    }
    
    .fade-out {
      opacity: 1;
      animation-name: fade-out;
      animation-duration: 0.2s;
      animation-timing-function: ease-out;
      animation-fill-mode: forwards;
    }
    <div class="container">
      <div class="headerhide" id="headerhide">
        <div class="logo-container">
          <div class="logo"></div>
        </div>
      </div>
      <div class="header-background" id="region"></div>
    </div>