Search code examples
javascriptcsssticky

Detect if element is sticky when there are other sticky in viewport


I need to detect when a sticky element gets pinned/sticky.

with this approach is working fine if there is only one sticky element at the time. Note that the trick is setting top: -1px and checking if element is leaving the viewport

const stickyElements = document.querySelectorAll('.sticky-element');

const observer = new IntersectionObserver(
  ([entry]) => {
    entry.target.classList.toggle('pinned', entry.intersectionRatio < 1);
  },
  { threshold: [1, 0.01] },
);

stickyElements.forEach((sticky, i) => {
  if (sticky) {
    observer.observe(sticky);
  }
});
.sticky-element {
   position: sticky;
   top: -1px;
   z-index: 2;
}

.sticky-element.pinned {
   background: white;
   border-bottom: 1px solid #ccc;
}

.sticky-element.pinned {
   &:after {
        content: ' (pinned)';
   }
}

.just-content {
   height: 100vh;
   display: flex;
   justify-content: center;
   align-items: center;
   background: #f8f8f8;
}
<div class="sticky-element">I'm sticky</div>
<div class="just-content">I'm just normal element</div>
<div class="sticky-element">I'm sticky 1</div>
<div class="just-content">I'm just normal element</div>
<div class="sticky-element">I'm sticky 2</div>
<div class="just-content">I'm just normal element</div>

The issue is when you try to get it combined with an sticky element above, lets try it out:

const stickyElements = document.querySelectorAll('.sticky-element');

const observer = new IntersectionObserver(
  ([entry]) => {
    entry.target.classList.toggle('pinned', entry.intersectionRatio < 1);
  }, {
    threshold: [1, 0.01]
  },
);

stickyElements.forEach((sticky, i) => {
  if (sticky) {
    observer.observe(sticky);
  }
});
.sticky-nav {
  position: sticky;
  top: -1px;
  z-index: 2;
  outline: 1px dashed black;
}

.sticky-element {
  position: sticky;
  top: 15px;
  z-index: 2;
  outline: 1px dashed orange;
}

.sticky-element.pinned {
  background: white;
  border-bottom: 1px solid #ccc;
}

.sticky-element.pinned {
  &:after {
    content: ' (pinned)';
  }
}

.just-content {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f8f8f8;
}
<div class="just-content">I'm just normal element</div>
<nav class="sticky-nav">Sticky element just above</nav>
<div class="just-content">I'm just normal element</div>

  <div class="sticky-element">I'm sticky</div>
  <div class="just-content">I'm just normal element</div>
  <div class="sticky-element">I'm sticky 1</div>
  <div class="just-content">I'm just normal element</div>
  <div class="sticky-element">I'm sticky 2</div>
  <div class="just-content">I'm just normal element</div>

I added outlines so you can check that the items is behind/overlapping the "main" sticky element.

So the observer donesn't work in this scenario because the item is not actually leaving the viewport, i'm guessing.

so question is, any workaround for this scenario? perhaps container based?


Solution

  • I was able to achieve it by comparing the window.getComputedStyle(sticky, null).getPropertyValue('top') against the css top property value. When they match, is sticky

    const stickyElements = document.querySelectorAll('.sticky-element');
    
    const onScroll = () => {
      requestAnimationFrame(() => {
        stickyElements.forEach((sticky) => {
          if (sticky) {
            const elementCSSTop = parseInt(window.getComputedStyle(sticky, null).getPropertyValue('top'));
            sticky.classList.toggle('pinned', sticky.getBoundingClientRect().top === elementCSSTop);
          }
        });
      });
    };
    
    window.addEventListener('scroll', onScroll);
    .sticky-nav {
      position: sticky;
      top: -1px;
      z-index: 2;
      outline: 1px dashed black;
    }
    
    .sticky-element {
      position: sticky;
      top: 18px;
      z-index: 2;
    }
    
    .sticky-element.pinned {
      background: white;
      border-bottom: 1px solid #ccc;
    }
    
    .sticky-element.pinned {
      &:after {
        content: ' (pinned)';
      }
    }
    
    .just-content {
      height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      background: #f8f8f8;
    }
    <div class="just-content">I'm just normal element</div>
    <nav class="sticky-nav">Sticky element just above</nav>
    <div class="just-content">I'm just normal element</div>
    
    <div class="sticky-element">I'm sticky</div>
    <div class="just-content">I'm just normal element</div>
    <div class="sticky-element">I'm sticky 1</div>
    <div class="just-content">I'm just normal element</div>
    <div class="sticky-element">I'm sticky 2</div>
    <div class="just-content">I'm just normal element</div>