I'm using the new position: sticky
(info) to create an iOS-like list of content.
It's working well and far superior than the previous JavaScript alternative (example) however as far as I know no event is fired when it's triggered, which means I can't do anything when the bar hits the top of the page, unlike with the previous solution.
I'd like to add a class (e.g. stuck
) when an element with position: sticky
hits the top of the page. Is there a way to listen for this with JavaScript? Usage of jQuery is fine.
Setting position sticky Position sticky alternates the position of an element between relative and fixed based on the viewer's scroll position. A sticky element is relative to the document flow until a defined scroll position is reached, then it switches to behaving like a fixed element within its direct parent.
If the sticky element has a parent or ancestor with overflow: hidden , overflow: auto , or overflow: scroll , then position: sticky will not work properly.
// get the sticky element const stickyElm = document.querySelector('header') const observer = new IntersectionObserver( ([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1), {threshold: [1]} ); observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; } section{ background: lightblue; padding: 2em 1em; } header{ position: sticky; top: -1px; /* ➜ the trick */ padding: 1em; padding-top: calc(1em + 1px); /* ➜ compensate for the trick */ background: salmon; transition: .1s; } /* styles for when the header is in sticky mode */ header.isSticky{ font-size: .8em; opacity: .5; }
<section>Space</section> <header>Sticky Header</header>
The top
value needs to be -1px
or the element will never intersect with the top of the browser window (thus never triggering the intersection observer).
To counter this 1px
of hidden content, an additional 1px
of space should be added to either the border or the padding of the sticky element.
💡 Alternatively, if you wish to keep the CSS as is (top:0
), then you can apply the "correction" at the intersection observer-level by adding the setting rootMargin: '-1px 0px 0px 0px'
(as @mattrick showed in his answer)
scroll
event listener:scrollCallback
(to be able to unbind if needed)// get the sticky element const stickyElm = document.querySelector('header'); // get the first parent element which is scrollable const stickyElmScrollableParent = getScrollParent(stickyElm); // save the original offsetTop. when this changes, it means stickiness has begun. stickyElm._originalOffsetTop = stickyElm.offsetTop; // compare previous scrollTop to current one const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop) // Act if sticky or not const onSticky = isSticky => { console.clear() console.log(isSticky) stickyElm.classList.toggle('isSticky', isSticky) } // bind a scroll event listener on the scrollable parent (whatever it is) // in this exmaple I am throttling the "scroll" event for performance reasons. // I also use functional composition to diffrentiate between the detection function and // the function which acts uppon the detected information (stickiness) const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100) stickyElmScrollableParent.addEventListener('scroll', scrollCallback) // OPTIONAL CODE BELOW /////////////////// // find-first-scrollable-parent // Credit: https://stackoverflow.com/a/42543908/104380 function getScrollParent(element, includeHidden) { var style = getComputedStyle(element), excludeStaticParent = style.position === "absolute", overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/; if (style.position !== "fixed") for (var parent = element; (parent = parent.parentElement); ){ style = getComputedStyle(parent); if (excludeStaticParent && style.position === "static") continue; if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; } return window } // Throttle // Credit: https://jsfiddle.net/jonathansampson/m7G64 function throttle (callback, limit) { var wait = false; // Initially, we're not waiting return function () { // We return a throttled function if (!wait) { // If we're not waiting callback.call(); // Execute users function wait = true; // Prevent future invocations setTimeout(function () { // After a period of time wait = false; // And allow future invocations }, limit); } } }
header{ position: sticky; top: 0; /* not important styles */ background: salmon; padding: 1em; transition: .1s; } header.isSticky{ /* styles for when the header is in sticky mode */ font-size: .8em; opacity: .5; } /* not important styles*/ body{ height: 200vh; font:20px Arial; } section{ background: lightblue; padding: 2em 1em; }
<section>Space</section> <header>Sticky Header</header>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With