JavaScript is that magical creature that can add tremendous flavor to your interactions especially when its events like scrolling. The following is a review of properties available to authors that help detect scroll and position.
Properties
The following properties are listed with their associated object as they relate to scroll position…
window.pageXOffset
Alias for thewindow.scrollX
window.pageYOffset
Alias for thewindow.scrollY
window.scrollX
Distance from outer edge of browser the viewport scrolled horizontallywindow.scrollY
Distance from top of the viewport to the top of the scrollbar. Value will vary as you scroll downward.document.documentElement.scrollTop
Similar towindow.scrollY
(currently broken in WebKit based browsers)document.body.scrollTop
Wide support, but deprecated in strict mode
Offset Detection
Everyone loves something that sticks on scroll and in the following example I use window.pageYOffset
detection to “stick” our centered text when the text reaches the top of the viewport.
See the Pen Sticky Scroll Demo v1 by Dennis Gaebel (@grayghostvisuals) on CodePen.
To start things off I grab some variables and geometry as it’s always good to do the least amount of work inside a scroll handler (performance reasons). We retrieve the header and title selector along with both of their heights.
var header = document.querySelector('header'),
header_height = getComputedStyle(header).height.split('px')[0],
title = header.querySelector('h1'),
title_height = getComputedStyle(title).height.split('px')[0],
fix_class = 'is--fixed';
At this point I determine how far the scrollbar has moved using window.pageYOffset
(you could use scrollY
as well). If the scroll position is greater than (header_height - title_height ) / 2
then a CSS class is added otherwise we remove the class.
function stickyScroll(e) {
if( window.pageYOffset > (header_height - title_height ) / 2 ) {
title.classList.add(fix_class);
}
if( window.pageYOffset < (header_height - title_height ) / 2 ) {
title.classList.remove(fix_class);
}
}
Looking even closer, all the above takes place when the viewport has scrolled enough to have the top of the text touch the top of the viewport. This means when window.pageYOffset
is equal to the distance between the top of the window and the top of the text the text will “stick.”
To get at the math we know the text is in the middle of the viewport, so the vertical distance between the top edge of the text and the middle of the viewport is 1/2 it's height (refer to diagram in figure 4). This means the distance between the top of the window and the top of the text is 1/2 the viewport height (the vertical distance between the top of the viewport and middle middle of the viewport) minus half the text height. This is how we arrive at (header_height - title_height ) / 2
.
ScrollTop Detection
In this example scrollTop
is used to detect our trigger point at which we stick the main navigation to the top of the viewport. In this case we look for our scrollTop
distance to report greater than or equal to the header height. If this value is met then we toggle a class and show the nav strip.
See the Pen ScrollTop Demo by Dennis Gaebel (@grayghostvisuals) on CodePen.
scrollTop
and element property values for detectionFor this demo I determine what the height of the header and the hero is in order for me to get at my offset value (distance between my starting and ending point). In this particular instance I'm using the jQuery method $(window).scrollTop()
, but you could also use document.body.scrollTop
if you desire a vanilla approach. You can see all this taking place in Figure 6.
// Config
// =================================================
var $nav_header = $('.banner'),
header_height = $('.banner').height(),
hero_height = $('.hero').height(),
offset_val = hero_height - header_height;
// Method
// =================================================
function navSlide() {
var scroll_top = $(window).scrollTop();
if (scroll_top >= offset_val) { // the detection!
$nav_header.addClass('is-sticky');
} else {
$nav_header.removeClass('is-sticky');
}
}
// Handler
// =================================================
$(window).scroll(navSlide);
Device Support
Back in the days prior to iOS 8, scroll events would never trigger unless your finger was removed from the screen. To prove how things have changed for the better here's a screencast of the iOS 7 scroll behavior (http://cl.ly/image/2I3g250Q052e). As you can see the text only sticks to the top when you lift your finger up. This is not due to iOS 7 pausing JavaScript execution, but pausing painting so your site's JavaScript will continue to run, but any changes to the DOM will not be painted until the scroll action completes.
Fast forward to current times and the results are far different now that iOS 8 has been released. As you can see in this screencast the scroll event reacts no matter if my finger is on the screen or not (http://cl.ly/image/3V0Z1N1R290B). The execution now triggers as it should (as the event takes place).
Notes
Tom Moitié discovered a strange result with window.scrollY
when using dual monitors and shifting the position of the main monitor (found under system preferences on Mac). The same happens for window.scrollLeft
with two monitors. For example, I have an Apple Studio Display (yup you heard that right) with a Samsung as my secondary monitor. The monitor on the right (Apple Studio Display) reports window.screenLeft
as 0, but the Samsung monitor reports -1920 when I drag my browser over to that monitor (Samsung).
References & Such
- https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollY
- https://developer.mozilla.org/en-US/docs/Web/API/Window.scrollX
- http://api.jquery.com/scrolltop
- http://developer.telerik.com/featured/scroll-event-change-ios-8-big-deal
- http://stackoverflow.com/questions/21268450/body-scrolltop-is-deprecated-in-strict-mode-please-use-documentelement-scrollt
Might be good idea to debounce that scroll event you got there
http://stackoverflow.com/questions/15927371/what-does-debounce-do/15927403#15927403
Hi Marco. In my tests I find that debouncing only delays the execution of scroll events. For example, the Fill Murray nav on scroll appears as it should, but is delayed from it’s initial trigger point. The effect becomes more jarring and the point of execution is missed entirely.
That might be true. You would still get (a lot) less calls on the
navSlide
though, having less overhead and possibly better FPS. Still, you could improve it further to make sure it’s optimized which reminds ofrequestAnimationFrame
: http://www.html5rocks.com/en/tutorials/speed/animationsThanks for sharing this thought out post! I specially like your diagram example, always nice to visualize the problem
You’re welcome Marco. Thanks for reading and sharing your tips plus starting a great discussion. It seems rAF in this particular case isn’t all that helpful. Chrome now schedules 1 scroll event per frame, just before rAF. You can see that paint times and FPS are far better without rAF.