Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by ZenithRanger515

How can I toggle a class based on scroll direction when an element is barely visible using Intersection Observer?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm aiming to toggle a class on an element based on scroll direction using Intersection Observer. Specifically, when scrolling down and the target element is just barely visible at the bottom of the viewport, I want to add a class; and when scrolling up, I want to remove it.

I currently have the following implementation, but the class isn't toggled reliably at the precise moment when the element is barely visible:

JAVASCRIPT
const options = { root: null, // rootMargin: '50px', threshold: 0.1 } const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { // up if (entry.boundingClientRect.top < 0) { document.querySelector('button').classList.remove('hidden'); } else { // down document.querySelector('button').classList.add('hidden') } } }, options ); const arr = document.querySelectorAll('h2') arr.forEach(i => { observer.observe(i) })
CSS
h2 { margin-top: 700px; margin-bottom: 700px; } button { position: fixed; top: 0; left: 0; } button.hidden { background: red !important; }
HTML
<h2> ELEMENT </h2> <button> FIXED BUTTON </button>

How can I adjust my Intersection Observer settings or logic so that the class is toggled exactly when the element becomes barely visible at the bottom of the screen?

1

Comments 0

Answers 2

Views 35

2 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

Below is one approach to fine‑tuning your Intersection Observer so that the callback fires when your target element is just barely visible at the bottom. The idea is to adjust the observer’s “viewport” using the rootMargin and then use the element’s boundingClientRect to decide when to add or remove your class.

For example, you can set a negative bottom margin so that the observer “thinks” the bottom of your viewport is a bit higher than it really is. This makes the callback fire when the element is almost leaving (or entering) at the bottom:


Improved JavaScript Example

JAVASCRIPT
const options = { root: null, // Set a bottom margin to trigger when the element is 100px into the viewport from the bottom rootMargin: '0px 0px -100px 0px', // Use a low threshold so that any visibility triggers the callback threshold: 0 }; const observer = new IntersectionObserver(([entry]) => { const button = document.querySelector('button'); // entry.boundingClientRect gives the element's position relative to the viewport // Check if the bottom of the element is near the viewport bottom. if (entry.isIntersecting && entry.boundingClientRect.bottom <= window.innerHeight) { // Element is just barely visible at the bottom; add the class (scrolling down) button.classList.add('hidden'); } else { // Otherwise, remove the class (scrolling up or fully in view) button.classList.remove('hidden'); } }, options); document.querySelectorAll('h2').forEach(el => observer.observe(el));

Explanation

  1. Adjust rootMargin:
    By setting
    rootMargin: '0px 0px -100px 0px'
    you are effectively moving the bottom boundary of your observer’s viewport 100px higher. The observer will fire its callback when your element reaches this “adjusted” bottom edge.

  2. Threshold:
    Using a threshold of 0 means that any appearance of the element in this adjusted viewport will trigger the callback.

  3. Deciding When to Toggle:
    In the callback, we check if the element is intersecting and whether its bottom edge is at or above the actual viewport bottom. When both are true, it means the element is just barely visible from the bottom, so we add the class (for example, to hide the button as you scroll down). Otherwise, we remove the class (for instance, if it’s coming from above or fully in view).

Adjust the values (e.g. the -100px) as needed so that the toggle happens exactly when you consider the element “barely visible.” This gives you more control over the timing compared to relying solely on the threshold value.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by AuroraNavigator313 1 month ago

0

The following example was adapted from this article. There is a bug (see comment) concerning .rootBounds so it replaced by a simple constant of 100. The class changes are applied to the entry.target, h2 (the scrolled element being observed) and the fixed element h1.

Details are commented in example.

JAVASCRIPT
const h1 = document.querySelector("h1"); const h2 = document.querySelector("h2"); /** * Having a rootMargin would mean the target element would have * to clear that length before it was considered intersecting * so it was changed from 50px to 0px */ const options = { root: null, rootMargin: "0px", threshold: 0 }; /** * Callback function compares the target element top position * to its bottom position in order to determine the direction * the target element is scrolling (up or down). * @param {object} entries - IntersectionObserverEntry * @param {object} observer - IntersectionObserver instance */ const callback = (entries, observer) => { entries.forEach(entry => { /** * IntersectionObserverEntry Properties * - .intersectionRect is the clientBoundingRect of the part * of the target that is intersecting. * - .top is the y position of .intersectionRect relative to * its top border and viewport. * - .bottom is like .top concerning the bottom border. * - .intersectionRatio is the portion of the target that's * intersecting. * - .target is the element being observed. * - .isIntersecting a boolean when true is when .target is * at or past its threshold and within the intersection * observer root. */ const fromTop = entry.intersectionRect.top; const fromBottom = 100 - entry.intersectionRect.bottom; if (entry.intersectionRatio < 0.1 || fromTop > fromBottom && entry.isIntersecting === true) { h1.classList.remove("red"); entry.target.classList.remove("red"); } if (fromBottom > fromTop && entry.isIntersecting === true) { h1.classList.add("red"); entry.target.classList.add("red"); } }); }; /** * Instintate IntersectionObserver pass callback function and * options object. */ let observer = new IntersectionObserver(callback, options); // Call .observe() method and pass the target element observer.observe(h2);
CSS
body { display: flex; justify-content: center; align-items: center; min-height: 350vh; } header { position: fixed; top: 0; left: 0 } .red { color: red; }
HTML
<header> <h1>Header 1</h1> </header> <h2>Header 2</h2>

Run code snippetHide resultsExpand snippet

No comments yet.

Discussion

No comments yet.