Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at RIA Unleashed (Nov. 2009) with: Elad Elrom
Ben Nadel at RIA Unleashed (Nov. 2009) with: Elad Elrom@EladElrom )

Automatically Scroll The Window When The User Approaches The Viewport Edge In JavaScript

By Ben Nadel on

CAUTION: This is primarily a "note to self" for future Ben. This represents a part of DOM interaction that I don't have a solid mental model for. As such, my confidence level here is not stellar.

The other day, I had to a fix a bug in InVision that related to dragging DOM (Document Object Model) elements up and down in the document. As the user's mouse approached the top or bottom edge of the browser viewport, we needed to scroll the viewport such that the user could drag the selected DOM element to a position that had not been previously-visible within the viewport. Because of some cross-browser quirkiness involved in the getting and setting of document and window dimensions and offsets, I wanted to break this logic out into a demo such that I could reference it at a later time. As such, the logic in this demo will automatically scroll the window when the user approaches the viewport edge.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

As I said above, there is some cross-browser insights that need to be taken into account when getting and setting document and window dimensions and offsets. I don't have the brain capacity to keep this information in my head. So, I find myself continually referring back to the relevant entry in JavaScript.info.

That said, for this demo, we want to start scrolling the window in a meaningful direction when the user moves their mouse towards the edge of the browser. And, we want the scrolling velocity to increase as the user gets closer to the edge. So, not only do we have to determine if the user's mouse is contained within the "edge" of the viewport, we have to determine how much of the edge has been crossed.

Furthermore, we want the window scrolling to be activated even after the user stops moving their mouse. Meaning, if the user moves their mouse to the "edge" of the viewport and then stops moving, we want the window to continue scrolling until it has reached its maximum scroll position. To accomplish this, I'm using an iterative setTimeout() call. I am sure something much more clever could be accomplished. But, for this demo, I was more concerned with the math and the positional calculations than I was with the smoothness and efficiency of the window scrolling itself.

That said, here is the code. The bulk of the logic is contained within the mousemove handler. The demo steps a 200-pixel "edge" on each side of the viewport that will cause the window to scroll:

  • <!doctype html>
  • <html lang="en">
  • <head>
  • <meta charset="utf-8" />
  • <title>
  • Automatically Scroll The Window When The User Approaches The Viewport Edge In JavaScript
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Automatically Scroll The Window When The User Approaches The Viewport Edge In JavaScript
  • </h1>
  •  
  • <script type="text/javascript">
  •  
  • var edgeSize = 200;
  • var timer = null;
  •  
  • window.addEventListener( "mousemove", handleMousemove, false );
  •  
  • drawGridLines();
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // I adjust the window scrolling in response to the given mousemove event.
  • function handleMousemove( event ) {
  •  
  • // NOTE: Much of the information here, with regard to document dimensions,
  • // viewport dimensions, and window scrolling is derived from JavaScript.info.
  • // I am consuming it here primarily as NOTE TO SELF.
  • // --
  • // Read More: https://javascript.info/size-and-scroll-window
  • // --
  • // CAUTION: The viewport and document dimensions can all be CACHED and then
  • // recalculated on window-resize events (for the most part). I am keeping it
  • // all here in the mousemove event handler to remove as many of the moving
  • // parts as possible and keep the demo as simple as possible.
  •  
  • // Get the viewport-relative coordinates of the mousemove event.
  • var viewportX = event.clientX;
  • var viewportY = event.clientY;
  •  
  • // Get the viewport dimensions.
  • var viewportWidth = document.documentElement.clientWidth;
  • var viewportHeight = document.documentElement.clientHeight;
  •  
  • // Next, we need to determine if the mouse is within the "edge" of the
  • // viewport, which may require scrolling the window. To do this, we need to
  • // calculate the boundaries of the edge in the viewport (these coordinates
  • // are relative to the viewport grid system).
  • var edgeTop = edgeSize;
  • var edgeLeft = edgeSize;
  • var edgeBottom = ( viewportHeight - edgeSize );
  • var edgeRight = ( viewportWidth - edgeSize );
  •  
  • var isInLeftEdge = ( viewportX < edgeLeft );
  • var isInRightEdge = ( viewportX > edgeRight );
  • var isInTopEdge = ( viewportY < edgeTop );
  • var isInBottomEdge = ( viewportY > edgeBottom );
  •  
  • // If the mouse is not in the viewport edge, there's no need to calculate
  • // anything else.
  • if ( ! ( isInLeftEdge || isInRightEdge || isInTopEdge || isInBottomEdge ) ) {
  •  
  • clearTimeout( timer );
  • return;
  •  
  • }
  •  
  • // If we made it this far, the user's mouse is located within the edge of the
  • // viewport. As such, we need to check to see if scrolling needs to be done.
  •  
  • // Get the document dimensions.
  • // --
  • // NOTE: The various property reads here are for cross-browser compatibility
  • // as outlined in the JavaScript.info site (link provided above).
  • var documentWidth = Math.max(
  • document.body.scrollWidth,
  • document.body.offsetWidth,
  • document.body.clientWidth,
  • document.documentElement.scrollWidth,
  • document.documentElement.offsetWidth,
  • document.documentElement.clientWidth
  • );
  • var documentHeight = Math.max(
  • document.body.scrollHeight,
  • document.body.offsetHeight,
  • document.body.clientHeight,
  • document.documentElement.scrollHeight,
  • document.documentElement.offsetHeight,
  • document.documentElement.clientHeight
  • );
  •  
  • // Calculate the maximum scroll offset in each direction. Since you can only
  • // scroll the overflow portion of the document, the maximum represents the
  • // length of the document that is NOT in the viewport.
  • var maxScrollX = ( documentWidth - viewportWidth );
  • var maxScrollY = ( documentHeight - viewportHeight );
  •  
  • // As we examine the mousemove event, we want to adjust the window scroll in
  • // immediate response to the event; but, we also want to continue adjusting
  • // the window scroll if the user rests their mouse in the edge boundary. To
  • // do this, we'll invoke the adjustment logic immediately. Then, we'll setup
  • // a timer that continues to invoke the adjustment logic while the window can
  • // still be scrolled in a particular direction.
  • // --
  • // NOTE: There are probably better ways to handle the ongoing animation
  • // check. But, the point of this demo is really about the math logic, not so
  • // much about the interval logic.
  • (function checkForWindowScroll() {
  •  
  • clearTimeout( timer );
  •  
  • if ( adjustWindowScroll() ) {
  •  
  • timer = setTimeout( checkForWindowScroll, 30 );
  •  
  • }
  •  
  • })();
  •  
  • // Adjust the window scroll based on the user's mouse position. Returns True
  • // or False depending on whether or not the window scroll was changed.
  • function adjustWindowScroll() {
  •  
  • // Get the current scroll position of the document.
  • var currentScrollX = window.pageXOffset;
  • var currentScrollY = window.pageYOffset;
  •  
  • // Determine if the window can be scrolled in any particular direction.
  • var canScrollUp = ( currentScrollY > 0 );
  • var canScrollDown = ( currentScrollY < maxScrollY );
  • var canScrollLeft = ( currentScrollX > 0 );
  • var canScrollRight = ( currentScrollX < maxScrollX );
  •  
  • // Since we can potentially scroll in two directions at the same time,
  • // let's keep track of the next scroll, starting with the current scroll.
  • // Each of these values can then be adjusted independently in the logic
  • // below.
  • var nextScrollX = currentScrollX;
  • var nextScrollY = currentScrollY;
  •  
  • // As we examine the mouse position within the edge, we want to make the
  • // incremental scroll changes more "intense" the closer that the user
  • // gets the viewport edge. As such, we'll calculate the percentage that
  • // the user has made it "through the edge" when calculating the delta.
  • // Then, that use that percentage to back-off from the "max" step value.
  • var maxStep = 50;
  •  
  • // Should we scroll left?
  • if ( isInLeftEdge && canScrollLeft ) {
  •  
  • var intensity = ( ( edgeLeft - viewportX ) / edgeSize );
  •  
  • nextScrollX = ( nextScrollX - ( maxStep * intensity ) );
  •  
  • // Should we scroll right?
  • } else if ( isInRightEdge && canScrollRight ) {
  •  
  • var intensity = ( ( viewportX - edgeRight ) / edgeSize );
  •  
  • nextScrollX = ( nextScrollX + ( maxStep * intensity ) );
  •  
  • }
  •  
  • // Should we scroll up?
  • if ( isInTopEdge && canScrollUp ) {
  •  
  • var intensity = ( ( edgeTop - viewportY ) / edgeSize );
  •  
  • nextScrollY = ( nextScrollY - ( maxStep * intensity ) );
  •  
  • // Should we scroll down?
  • } else if ( isInBottomEdge && canScrollDown ) {
  •  
  • var intensity = ( ( viewportY - edgeBottom ) / edgeSize );
  •  
  • nextScrollY = ( nextScrollY + ( maxStep * intensity ) );
  •  
  • }
  •  
  • // Sanitize invalid maximums. An invalid scroll offset won't break the
  • // subsequent .scrollTo() call; however, it will make it harder to
  • // determine if the .scrollTo() method should have been called in the
  • // first place.
  • nextScrollX = Math.max( 0, Math.min( maxScrollX, nextScrollX ) );
  • nextScrollY = Math.max( 0, Math.min( maxScrollY, nextScrollY ) );
  •  
  • if (
  • ( nextScrollX !== currentScrollX ) ||
  • ( nextScrollY !== currentScrollY )
  • ) {
  •  
  • window.scrollTo( nextScrollX, nextScrollY );
  • return( true );
  •  
  • } else {
  •  
  • return( false );
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  •  
  • // I draw the grid and edge lines in the document so that it is clear where
  • // scrolling will be initiated and with what intensity it is taking place.
  • function drawGridLines() {
  •  
  • var increment = 100;
  • var incrementCount = 100;
  • var maxDimension = ( increment * incrementCount );
  •  
  • // Draw the boxes that make up the grid.
  • for ( var x = 0 ; x < incrementCount ; x++ ) {
  • for ( var y = 0 ; y < incrementCount ; y++ ) {
  •  
  • var xOffset = ( x * increment );
  • var yOffset = ( y * increment );
  •  
  • var box = document.createElement( "span" );
  • box.style.position = "absolute";
  • box.style.top = ( yOffset + "px" );
  • box.style.left = ( xOffset + "px" );
  • box.style.height = ( increment + "px" );
  • box.style.width = ( increment + "px" );
  • box.style.border = "1px solid #CCCCCC";
  • box.style.font = "11px sans-serif";
  • box.style.color = "#999999" ;
  • box.style.boxSizing = "border-box";
  • box.style.padding = "5px 5px 5px 5px";
  • box.innerText = ( xOffset + ", " + yOffset );
  • document.body.appendChild( box );
  •  
  • }
  • }
  •  
  • // Draw the edges that delineate the scrolling zone.
  • var edge = document.createElement( "span" );
  • edge.style.position = "fixed";
  • edge.style.top = ( edgeSize + "px" );
  • edge.style.bottom = ( edgeSize + "px" );
  • edge.style.left = ( edgeSize + "px" );
  • edge.style.right = ( edgeSize + "px" );
  • edge.style.border = "2px solid #CC0000";
  • edge.style.borderRadius = "5px 5px 5px 5px";
  • document.body.appendChild( edge );
  •  
  • // Add mouse-guard so that nothing is selectable.
  • var guard = document.createElement( "div" );
  • guard.style.position = "fixed";
  • guard.style.top = "0px";
  • guard.style.bottom = "0px";
  • guard.style.left = "0px";
  • guard.style.right = "0px";
  • document.body.appendChild( guard );
  •  
  • }
  •  
  • </script>
  •  
  • </body>
  • </html>

Since this is primarily a note-to-self, I'm not going to add much explanation beyond the comments that are in the code. Now, if we open this in the browser and try moving the mouse to the edge of the viewport, we can see the viewport start scrolling automatically with a dynamic intensity:


 
 
 

 
 Automatically scrolling the window when the user's mouse approaches the edge of the viewport. 
 
 
 

Obviously, because this is a demo about movement, the effects are much easier to see in the video than they are in an annotated screenshot.

Anyway, like I said, this was primarily a note-to-self. But, perhaps this may help others who have to deal with automatically scrolling the window when the user approaches the edge of the viewport. If nothing else, this can point you in the direction of the JavaScript.info page that has information about all of the cross-browser considerations that need to be accounted for when dealing with document and window dimensions and offsets.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

This is an interesting exploration. I do remember that prototype.js Scriptaculous draggable, has an option setting that does just what you are trying to achieve. It would be interesting to compare their approach! I used to be an avid Scriptaculous fan, before I discovered JQuery.

Reply to this Comment

@Charles,

I knew of Scriptaculous, but I think jQuery was my first real library. Before jQuery, I used some tiny one called sack.js that did nothing put grab the contents of a URL and inject it as the .innerHTML of some target element. Which, was still pretty cool :)

That said, dragging stuff is easily one of the most complicated things on the web, as far as I can see. Especially when you start to bring the idea of creating "ghost" elements. And, sprinkle in the fact that many UIs are data-driven now. Like, what happens when some Angular / React digest runs mid-drag? Do you change the element, mid-drag? Do you detach it from data-binding during the drag? These are not easy questions to answer.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.