Rik Schennink documents a system for being able to write CSS selectors that style a page when it has scrolled to a certain point. If you’re like me, you’re already on the lookout for document.addEventListener('scroll' ...
and being terrified about performance. Rik gets to that right away by both debouncing the function as well as marking the event as passive
.
The end result is a data-scroll
attribute on the <html>
element that can be used in the CSS. Meaning if you’re scrolled to 640px down the page, you have <html data-scroll="640">
and could write a selector like:
html:not([data-scroll='0']) {
padding-top: 3em;
}
html:not([data-scroll='0']) header {
position: fixed;
}
See the Pen
Writing Dumb JS 🧟♂️ and Smart CSS 👩🔬 by Rik Schennink (@rikschennink)
on CodePen.
Unfortunately, we don’t have greater than (>
) less than (<
) selectors in CSS for things like numbered attributes, so the CSS styling potential is fairly limited here. You might ultimately need to update the JavaScript function such that it applies other classes or data attributes based on your math. But you’ll already be set up for good performance here.
“Apply styles when the user has scrolled away from the top” is a legit use case. It makes me think of a once function (like we have in jQuery) where any scroll event would only be triggered once and then not again. They scrolled! So, by definition, they aren’t at the top anymore! But that doesn’t deal with when they scroll back to the top.
I find it generally more useful to use IntersectionObserver
for styling things based on scroll position. With it, you can do things like, “has this element been scrolled into view or beyond,” which is generically useful and can be used for scrolled-away-from-top stuff too.
Here’s an example that adds or removes a class if a user has scrolled past a hidden pixel positioned at 500px down the page.
See the Pen
Fixed Header with IntersectionObserver by Chris Coyier (@chriscoyier)
on CodePen.
That’s performant as well, avoiding any scroll event handlers at all.
And speaking of IntersectionObserver
, check out “Trust is Good, Observation is Better—Intersection Observer v2”.
I think that using the passive option on the scroll listener is redundant since you can’t prevent it anyway. The reason is that the scroll event is fired after the scroll already happened in the browser. However, the wheel event can be prevented so we better use the passive option on its listener when we can.
IntersectionObserver – huh, def cool… but no IE11 support makes it unusable in production unfortunately.
FYI: In the future we will have range queries, see https://drafts.csswg.org/mediaqueries-4/#mq-range-context
The question that comes to mind, how far should we go within css? Logic can easily be added using JavaScript and just setting classes. Which might make more sense. Just like the example with IntersectionObserver does.
And I believe setting a class once is more performant than updating an html attribute all the time.
I’m also curious how often this type of debounce actually debounces, on my dev machine it does none. I still prefer a time based debounce with inside the callback just a single requestAnimationFrame. Or in this case a throttle.
I made a logic error in my scroll position since it goes above 100% but I made a simple demo to actually change styles on any scroll position using css vars. Note that it works in the stable version of Chrome but combining css-vars and calc inside css values is still risky.(didn’t work in firefox last time I tested something similar in Firefox)
seems “in window” is a listener the is effectively constant, so how does it compare to replacing the scroll event with requestAnimationFrame a la https://gist.github.com/Warry/4254579?
Actually, there’s an official w3c polyfill for IntersectionObserver: https://github.com/w3c/IntersectionObserver/tree/master/polyfill
See an example of it in action here: https://200.cravath.dev/industrialization-civil-war
I preferer using the sticky elements and IO for spies.
https://developers.google.com/web/updates/2017/09/sticky-headers#introducing_the_sticky-change_event
Learned some new stuff Thanks,. Normally I used to animate with document.addEventListener(‘scroll’ …) but I’m curious to know the performance issues with this approach.
Is the second example (using IntersectionObserver) intended to work stand alone? Or does it require the code from the first example? Either way, I get an error:
Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'.
Thank you.ADMIN EDIT:
Make sure the DOM is ready first ;)
For anyone else relatively new to the game, the markup in the example is SCSS, not CSS. However, even after converting the SCSS to CSS I get the same error.
I was able to resolve the Javascript error (and get this working) by adding the JavaScript directly in the document below the elements it references. If anyone knows of a way to achieve this functionality while keeping the Javascript in its own external file (referenced in the head element) I’d love to hear it. Thank you.