Detect when window vertical scrollbar appears

49,189

Solution 1

Sorry to bring this back from the dead but I have just run in to this limitation and came up with my own solution. It's a bit hacky but stick with me ...

The idea is to add a 100% width invisible iframe to the page and listen for resize events on it's internal window. These events will pick up changes not only to the outer window's size but also when scrollbars get added to or removed from the outer window.

It triggers a regular window resize event so it requires no extra code if you are already listening for window resize.

Tested in IE9 and Chrome/Firefox latest - could maybe be made to work in older IEs but my project doesn't support those so I haven't tried.

https://gist.github.com/OrganicPanda/8222636

Solution 2

Based on OrganicPanda's answer, came up with this jquery thing

$('<iframe id="scrollbar-listener"/>').css({
    'position'      : 'fixed',
'width'         : '100%',
'height'        : 0, 
'bottom'        : 0,
'border'        : 0,
'background-color'  : 'transparent'
}).on('load',function() {
    var vsb     = (document.body.scrollHeight > document.body.clientHeight);
    var timer   = null;
    this.contentWindow.addEventListener('resize', function() {
        clearTimeout(timer);
        timer = setTimeout(function() {
            var vsbnew = (document.body.scrollHeight > document.body.clientHeight);
            if (vsbnew) {
                if (!vsb) {
                    $(top.window).trigger('scrollbar',[true]);
                    vsb=true;
                }
            } else {
                if (vsb) {
                    $(top.window).trigger('scrollbar',[false]);
                    vsb=false;
                }
            }
        }, 100);
    });
}).appendTo('body');

This will trigger 'scrollbar' events on the window, if they appear/dissapear

Works on chrome/mac, at least. now, someone extend this to detect horizontal scrollbars :-)

Solution 3

The Scoop

It is possible to detect changes in scrollbar visibility by using ResizeObserver to check for changes in the size of the element that may take scrollbars and changes in the size of its contents.

Rationale

I started implementing a solution with the <iframe> method but quickly found that having a complete implementation required breaking the separation of concerns among the views of my application. I have a parent view which needs to know when a child view acquires a vertical scrollbar. (I don't care about the horizontal scrollbar.) I have two situations that may affect the visibility of the vertical scrollbar:

  1. The parent view is resized. This is under direct control of the user.

  2. The child view's contents becomes bigger or smaller. This is under indirect control of the user. The child view is showing the results of a search. The quantity and type of results determine the size of the child view.

I found that if I used <iframe> I'd have to muck with the child view to support the parent's needs. I prefer the child to not contain code for something which is purely a concern of the parent. With the solution I describe here, only the parent view needed to be modified.

So in looking for a better solution, I found this answer by Daniel Herr. He suggests using ResizeObserver to detect when a div's dimensions change. ResizeObserver is not yet available natively across browsers but there is a robust ponyfill/polyfill that I use for support in cases where native support is not available. (Here is the spec for ResizeObserver.)

Proof-of-Concept

I use this polyfill in its ponyfill mode. That way, the global environment remains untouched. This implementation relies on window.requestAnimationFrame, and will fall back on setTimeout for platforms that don't support window.requestAnimationFrame. Looking at the support for requestAnimationFrame on "Can I use...?", what I see there does not bother me. YMMV.

I have a live proof-of-concept. The key is to listen to changes in size on the DOM element that can accept scroll bars (the element with id container, in green) and listen to changes in size on the content that may need scrolling (the element with id content). The proof-of-concept uses interact.js to manage a resizer element (with id resizer, in blue) that allows resizing container. If you drag the bottom right corner of resizer, it will resize both resizer and container. The two buttons allow simulating changes in the size of the contents displayed by container.

I'm using this method in code that is currently at a pre-release stage, meaning it passed tests on multiple browsers, and is being evaluated by stakeholders, but is not yet in production.

The HTML:

<!DOCTYPE html>
<html>

<head>
  <script data-require="interact.js@*" data-semver="1.0.26" src="//rawgit.com/taye/interact.js/v1.0.26/interact.js"></script>
  <script src="//rawgit.com/que-etc/resize-observer-polyfill/master/dist/ResizeObserver.global.js"></script>
  <link rel="stylesheet" href="style.css" />
</head>

<body>
  <div id="resizer">
    <div id="container">
      <ul id="content">
        <li>Something</li>
      </ul>
    </div>
  </div>
  <button id="add">Add to content</button>
  <button id="remove">Remove from content</button>
  <p>Scroll bar is: <span id="visibility"></span></p>
  <ul id="event-log"></ul>
  <script src="script.js"></script>
</body>

</html>

The JavaScript:

var container = document.getElementById("container");
var resizer = document.getElementById("resizer");
interact(resizer)
  .resizable({
    restrict: {
      restriction: {
        left: 0,
        top: 0,
        right: window.innerWidth - 10,
        bottom: window.innerHeight - 10
      }
    }
  })
  .on('resizemove', function(event) {
    var target = resizer;

    var rect = target.getBoundingClientRect();

    var width = rect.width + event.dx;
    var height = rect.height + event.dy;
    target.style.width = width + 'px';
    target.style.height = height + 'px';
  });

var content = document.getElementById("content");
var add = document.getElementById("add");
add.addEventListener("click", function() {
  content.insertAdjacentHTML("beforeend", "<li>Foo</li>");
});

var remove = document.getElementById("remove");
remove.addEventListener("click", function() {
  content.removeChild(content.lastChild);
});

// Here is the code that pertains to the scrollbar visibility

var log = document.getElementById("event-log");
content.addEventListener("scrollbar", function () {
  log.insertAdjacentHTML("beforeend", "<li>Scrollbar changed!</li>");
});

var visiblity = document.getElementById("visibility");
var previouslyVisible;
function refreshVisibility() {
  var visible = container.scrollHeight > container.clientHeight;
  visibility.textContent = visible ? "visible" : "not visible";
  if (visible !== previouslyVisible) {
    content.dispatchEvent(new Event("scrollbar"));
  }
  previouslyVisible = visible;
}
// refreshVisibility();


var ro = new ResizeObserver(refreshVisibility);
ro.observe(container);
ro.observe(content);

The CSS:

* {
  box-sizing: border-box;
}

#container {
  position: relative;
  top: 10%;
  left: 10%;
  height: 80%;
  width: 80%;
  background: green;
  overflow: auto;
}

#resizer {
  background: blue;
  height: 200px;
  width: 200px;
}

Solution 4

If you're using AngularJS, you can use a directive to detect when the width changes (assuming the appearing/disappearing scrollbar is a vertical one):

app.directive('verticalScroll', function($rootScope){
    return {
        restrict: 'A',
        link: function (scope, element) {
            scope.$watch(
                function() {
                    return element[0].clientWidth;
                },
                function() {
                    $rootScope.$emit('resize');
                }
            );
        }
    }
});

This fires an event on the root scope which other directives or controllers can listen for.

The watch is fired by the angular digest loop, so this relies on Angular having loaded/removed the extra content which has caused your scrollbar to appear/disappear.

Share:
49,189
Roman
Author by

Roman

programmer

Updated on July 09, 2022

Comments

  • Roman
    Roman almost 2 years

    Is there a simple and reliable solution for detecting window vertical scrollbar appears/disappears?

    window.onresize isn't triggered when after JavaScript DOM manipulation page becomes high enough for appearing scrollbar.

    In this very similar post Detect if a page has a vertical scrollbar described solution how to detect whether scrollbar is present or not, but I need to know when exactly it appears.

  • Mark Lopez
    Mark Lopez almost 10 years
    I think the OP wanted an event that could be handled - detecting scrollbars can be easily done with faster methods (?).
  • Dan King
    Dan King over 9 years
    Nice solution! Unfortunately it doesn't work in IE7 or IE8 (I haven't tried in 9-11), but I guess that isn't too much of a concern for most people...
  • Adam McCormick
    Adam McCormick about 8 years
    For anyone working in React, I've adapted this to a React component gist.github.com/AdamMcCormick/d5f718d2e9569acdf7def25e8266bb‌​2a
  • Louis
    Louis over 7 years
    Problems with this answer. First, the link has gone bad. Secondly, as the previous commenter pointed out, the issue the OP wants to solve is not just how to detect that there is a scrollbar but how to get an event notifying the code that a scrollbar has become visible or become hidden. Third, the way this answer is written, it is really hard to make out what it is supposed to say.
  • Gustavo Ferrufino
    Gustavo Ferrufino about 6 years
    can you please give an example how another directive would call this verticalScroll. @Dan
  • curran
    curran almost 5 years
  • Vladimir Jovanović
    Vladimir Jovanović almost 4 years
    You mean window.innerHeight?
  • Blair Connolly
    Blair Connolly almost 4 years
    I haven't tested this, but it sounds like this would NOT work on a Mac, since their scrollbars do not take away from the width of the viewport.
  • OrganicPanda
    OrganicPanda almost 4 years
    The scrollbars on Mac/iOS do not take up space when they appear they only overlay the page when scrolling so there's no "resize" to worry about. For that reason this trick would still work for the intended use case of "know when the size of the document changes because of a vertical scrollbar". If your use case is more like "know when this element overflows the viewport and causes scrolling" this will not work. Both use cases are better served by IntersectionObserver/ResizeObserver these days but this answer was cool in 2014