6

I've got a navigation component that consists of a skinny banner (that should hide on toward scroll) and a main-nav that should then stick to top & and shrink.

I've followed the popular answer on using Intersection Observer: Event to detect when position:sticky is triggered

But the issue with that solution is that my child elements (flex) cause a flicker when going between the hidden and shown sticky banner. I can't remove those child elements for obvious reasons so instead I'm opting for a position: fixed on the main-nav with a top: 40px. This means the skinny-banner scrolls away as desired, but I need help getting the scrollbar position with Javascript, and then adding a class like (.isSticky when the skinny-banner is no longer there to ensure the main-nav sticks to the top).

.isSticky {
 top: 0;
 height: 66px;
}

body { 
 margin: 0;
 height: 200vh;  
}

.skinny-banner{
  background: lightblue;
  height: 40px;
  display: flex;
}

.nav-menu {
  display: flex;
}

.sticky-nav{
  position: fixed;
  top: 40px;                    


  background: salmon;
  transition: .1s;
}

/* styles for when the header is in sticky mode */
.sticky-nav.isSticky{
  top: 0;
  height: 66px;
}
<header>
   <div class="skinny-banner">Skinny banner that on scroll down disapears.</div>
   <div class="sticky-nav">Sticky Header that on scroll down sticks to top and shrinks in height when stuck</div>
</header>

I'm hoping to use a vanilla JS, HTML & CSS solution & want to maintain the HTML structure with a wrapping container with the skinny-banner and nav-menu as children.

cts
  • 908
  • 1
  • 9
  • 30
  • I already gave you a duplicate in your previous question that solves your issue using only CSS – Temani Afif May 05 '23 at 15:53
  • I outlined in the OP that this question required a different approach due to child elements flex properties causing a "flicker" – cts May 05 '23 at 21:57
  • With jQuery you can easily check if a certain element is visible on the page or not: `$('your-element').is(':visible')`. With pure javascipt there are a few more steps involved hence why I mention jQuery. – Romeo Beun May 07 '23 at 17:23

3 Answers3

2

The reason for .sticky-nav fails to get sticky is because it is inside of the <header> element. That element has a default display: block according to w3school. So it will block the sticky behavior for its child elements (include position: sticky). To get this done, you should set the display: initial for the <header> element.

You can also detect when the element gets sticky with the value of .offsetTop. The value will change if the element gets sticky. But, before you should specify position: sticky and top: 0 for .sticky-nav.

const stickyNav = document.querySelector('.sticky-nav');
const initalPos = stickyNav.offsetTop;

window.addEventListener("scroll", () => {
  if(stickyNav.offsetTop > initalPos) {
    stickyNav.classList.add('isSticky');
  } else {
    stickyNav.classList.remove('isSticky');
  }
});
body { 
 margin: 0;
 height: 200vh;  
}
header{
  display: initial;
}
.skinny-banner{
  background: lightblue;
  height: 40px;
  display: flex;
}

.nav-menu {
  display: flex;
}

.sticky-nav{
  position: sticky;
  top: 0;
  background: salmon;
  min-height: 1px; /* custom this */
  transition: min-height ease 1s; /* custom this */
}

/* styles for when the header is in sticky mode */
.sticky-nav.isSticky{
  top: 0;
  min-height: 66px;  /* custom this */
}
<header>
   <div class="skinny-banner">Skinny banner that, on scroll down, disappears.</div>
   <div class="sticky-nav">Sticky Header that, on scroll down, sticks to top and shrinks in height when stuck</div>
   <div>Test content 1</div>
   <div>Test content 2</div>
   <div>Test content 3</div>
   <div>Test content 4</div>
   <div>Test content 5</div>
   <div>Test content 6</div>
   <div>Test content 7</div>
   <div>Test content 8</div>
   <div>Test content 9</div>
   <div>Test content 10</div>
   <div>Test content 11</div>
   <div>Test content 12</div>
   <div>Test content 13</div>
   <div>Test content 14</div>
   <div>Test content 15</div>
   <div>Test content 16</div>
</header>
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
Jordy
  • 1,802
  • 2
  • 6
  • 25
  • Jordy, thanks for this: it's exactly how I'd want it to behave. One question: when there is other content on the same level as `Header` - the flicker is visible again? https://jsfiddle.net/98ecm7s0/35/ – cts May 09 '23 at 13:05
  • @cts You're welcome. Flicker behavior can be solved with `min-height` as stated in the updated answer above. – Jordy May 09 '23 at 13:10
  • I think this circles back to the issue outlined in the OP: using `sticky` in this way only works if the child elements don't use flex, otherwise: we have the infamous "flicker": https://jsfiddle.net/98ecm7s0/37/ – cts May 09 '23 at 13:13
  • Not to worry anyway, the answer below is complete. Thanks for your help! – cts May 09 '23 at 13:36
  • @cts You're welcome, then give the check mark if you satisfied. – Jordy May 09 '23 at 13:56
  • 1
    Nice: that seems simpler than my answer, and preserve the transition effect. (I set it to 1 second just to better see said transition: it looks cool!) – VonC May 09 '23 at 15:38
0

It seems easier to set up a JavaScript listening to the scroll event on the window and updating the isSticky class accordingly based on the scroll position.

I also removed the transition, in case it participates in the flicker you see.

body {
  margin: 0;
  height: 200vh;
}

.skinny-banner {
  background: lightblue;
  height: 40px;
  display: flex;
}

.nav-menu {
  display: flex;
}

.sticky-nav {
  background: salmon;
  padding: 0 16px;
}


/* styles for when the header is in sticky mode */

.wrapper.isSticky .sticky-nav {
  position: fixed;
  top: 0;
  height: 66px;
  width: 100%;
}

.wrapper.isSticky .skinny-banner {
  display: none;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <header>
    <div class="wrapper">
      <div class="skinny-banner">Skinny banner that on scroll down disappears.</div>
      <div class="sticky-nav">Sticky Header that on scroll down sticks to top and shrinks in height when stuck</div>
      <div>Test content 1</div>
      <div>Test content 2</div>
      <div>Test content 3</div>
    </div>
  </header>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const wrapper = document.querySelector('.wrapper');
      const skinnyBanner = document.querySelector('.skinny-banner');
      let previousScrollY = window.scrollY;

      function updateSticky() {
        const currentScrollY = window.scrollY;

        if (currentScrollY > skinnyBanner.offsetHeight) {
          wrapper.classList.add('isSticky');
        } else {
          wrapper.classList.remove('isSticky');
        }

        previousScrollY = currentScrollY;
      }

      window.addEventListener('scroll', updateSticky);
    });
  </script>
</body>

</html>

To make sure the red banner sticky-nav should stick to the top even when scrolling up, the isSticky class is added to the wrapper element as soon as the user scrolls past the height of the skinny-banner, making the sticky-nav stick to the top regardless of the scroll direction.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Thanks for your response, but the solution ignores the requirement of having a wrapping div of which the `skinny-banner` & `sticky-nav` are child elements. – cts May 07 '23 at 20:32
  • @cts True. I have edited the answer to include a wrapping `div` of which the `skinny-banner` & `sticky-nav` are child elements. – VonC May 07 '23 at 20:48
  • thanks for your reply and making the update. Sadly when you scroll to the bottom of the viewport, and then scroll back up, the `sticky-nav` is not visible. – cts May 07 '23 at 20:52
  • @cts When I scroll back up, I confirm the sticky-nav is visible. Both are. – VonC May 07 '23 at 20:54
  • The `sticky-nav` should be visible at all times. – cts May 07 '23 at 21:06
  • @cts I confirm, the `sticky-nav` (the red banner with the "Sticky Header that on scroll down sticks to top and shrinks in height when stuck" in it) is visible at all times. – VonC May 07 '23 at 21:17
  • Having tried it both in the Stack Overflow snippet & a seperate codepen, my experience is that on scroll down: the skinny banner disappears, the sticky-nav increases in size, but once at the bottom of the viewport, and then scrolling up, neither are visible until both become visible at the non-sticky heights. – cts May 07 '23 at 23:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253531/discussion-between-vonc-and-cts). – VonC May 08 '23 at 09:36
0

Thank you for sharing your experience and insights! Please try out the following solution that might be helpful.

  window.onscroll = function() {stickyNav()};

  function stickyNav() {
    let navbar = document.querySelector(".sticky-nav");
    let banner = document.querySelector(".skinny-banner");      
    let sticky = navbar.offsetTop;
    if (window.pageYOffset >= sticky) {
      navbar.classList.add("isSticky");
  banner.classList.add("hide-banner"); 

    } else {
      navbar.classList.remove("isSticky");
      banner.classList.remove("hide-banner");
    }
  }
    body { 
      margin: 0;
      height: 200vh;  
    }

    .skinny-banner {
      background: lightblue;
      height: 40px;
      display: flex;
    }

    .nav-menu {
      display: flex;
    }

    .sticky-nav {
      position: fixed;
      top: 0px;
      background: salmon;
      transition: .1s;
    }

    /* styles for when the header is in sticky mode */
    .sticky-nav.isSticky {
      top: 0;
      height: 77px;
    }

    .hide-banner {
    display: none;
     }

    div {
      padding: 20px;
      background-color: #f2f2f2;
      border: 1px solid #ddd;
      margin: 10px;
    }
<div class="sticky-nav">Sticky Header that on scroll down sticks to top and shrinks in height when stuck</div>
<div class="skinny-banner">Skinny banner that on scroll down disapears.</div>      
<div>Test Content</div>
<div>Test Content</div>
<div>Test Content</div>
Faiq Daud
  • 1
  • 1