Detecting Scroll Position

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…

  1. window.pageXOffset Alias for the window.scrollX
  2. window.pageYOffset Alias for the window.scrollY
  3. window.scrollX Distance from outer edge of browser the viewport scrolled horizontally
  4. window.scrollY Distance from top of the viewport to the top of the scrollbar. Value will vary as you scroll downward.
  5. document.documentElement.scrollTop Similar to window.scrollY (currently broken in WebKit based browsers)
  6. 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.


Figure 1 : Sticky Scroll using offset detection

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';

Figure2 : Sticky Scroll Offset variables

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);
  }
}

Figure 3 : Sticky Scroll offset method

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.”


Figure 4 : Sticky Scroll offset diagram

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.


Figure 5 : Fill Murray demo using scrollTop and element property values for detection

For 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);

Figure 6 : setup for the detection of scrollTop and offset

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

Dennis Gaebel

Design Technologist, fly fisherman and guitar shredder making stuff out of browser native technologies. Co-Pilot for Open Source projects like Typeplate (Smashing Magazine Feature) and the A11YProject. Helper Bee at CSS-Tricks and Happy blogger for Web Design Weekly. Owner at Gray Ghost Visuals. Say hi to me on Twitter.
  1. Might be good idea to debounce that scroll event you got there :-)
    http://stackoverflow.com/questions/15927371/what-does-debounce-do/15927403#15927403

    1. 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.

      1. 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 of requestAnimationFrame : http://www.html5rocks.com/en/tutorials/speed/animations

        Thanks for sharing this thought out post! I specially like your diagram example, always nice to visualize the problem :)

      2. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *

show formatting examples
<pre class="language-[markup | sass | css | php | javascript | ruby | clike | bash]"><code>
…code example goes here…
</code></pre>

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Comment Preview

  1. John Doe shouted this comment preview:
    2014/10/14