Reputation: 4611
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.
Upvotes: 50
Views: 56920
Reputation: 1613
You can use window.visualViewport?.addEventListener('resize', update);
for this nowadays. It was introduced in 2019 and supported by basically every modern browser. The resize event triggers for any resize, including the opening of the on-screen keyboard on iOS and the scrollbar appearing.
Upvotes: 2
Reputation: 464
Combining the ResizeObserver API solutions and some solution for detecting if a scrollbar exists or not, I came up with the following:
// Create the ResizeObserver.
const resizeObserver = new ResizeObserver(() => {
if (document.documentElement.scrollHeight > document.documentElement.clientHeight) {
console.log('A vertical scrollbar appeared!');
// Your code to deal with a vertical scrollbar...
}
if (document.body.scrollWidth > document.body.clientWidth) {
console.log('A horizontal scrollbar appeared!');
// Your code to deal with a horizontal scrollbar...
}
});
// Observe the document body, or any HTML element whose size you expect to change when a scrollbar might appear.
resizeObserver.observe(document.body);
// Not part of the solution, but some code to illustrate the result:
const content = document.getElementById('content');
// Set some large text after 2 seconds to see the effect of a vertical scrollbar appearing.
setTimeout(() => {
content.innerHTML = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
}, 2000);
// After 2 more seconds, make sure the text does not wrap to see the effect of a horizontal scrollbar appearing.
setTimeout(() => content.style.whiteSpace = 'nowrap', 4000);
body {
margin: 0;
padding: 0;
}
#content {
font-size: 40px;
}
<p id="content"></p>
Upvotes: 0
Reputation: 1813
If you only need to detect the scroll appearance on Windows browsers (except IE), here's my solution with Resize Observer API for vertical scroll as an example.
<div>
with position: fixed
to <body>
<div>
's width, which in turn calls the observer callback.Mobile and macOS browsers have a disappearing scroll that is taken out of the document flow and doesn't affect the page layout.
fixed
and not absolute
?Element with position: fixed
is positioned relative to the initial containing block established by the viewport.
position: absolute
may fail if the <body>
is also absolutely positioned and has a different width than the viewport.
const innerWidthFiller = document.createElement('div')
innerWidthFiller.style.cssText = 'position: fixed; left: 0; right: 0'
document.body.appendChild(innerWidthFiller)
const detectScroll = () => {
const {clientHeight, scrollHeight} = document.documentElement
window.result.value = scrollHeight > clientHeight
}
const resizeObserver = new ResizeObserver(detectScroll)
resizeObserver.observe(innerWidthFiller)
#test {
border: 1px solid;
white-space: nowrap;
}
output {
font-weight: bold;
}
<button onclick="test.style.fontSize='100vh'">Enlarge the text</button>
<button onclick="test.style.fontSize=''">Reset</button>
Page scroll state: <output id="result"></output>
<hr>
<span id="test">Test element</span>
Upvotes: 0
Reputation: 727
It's all about when you need to determine the scrollbar's visibility.
The OP speaks of a time "after JavaScript DOM manipulation". If that manipulation happens in your code, then that's the time for checking if the scrollbar is visible. Why do you need an event in addition to that? How is it that you don't know when this DOM manipulation occurs?
I realize this is an old question, but I'm just now dealing with this in a pure javascript project, and I have no issue knowing when to check for scrollbar visibility. Either a user event fires, or a system event fires, and I know when the DOM manipulation occurs because I'm causing it via javascript. I don't see a case where that javascript DOM manipulation is outside of my code's awareness.
Maybe a scrollbarVisibilityChange event would be convenient, but it's certainly not necessary. This strikes me as a non-issue, 9 years later. Am I missing something?
Upvotes: 0
Reputation: 151401
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.
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:
The parent view is resized. This is under direct control of the user.
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
.)
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;
}
Upvotes: 6
Reputation: 3580
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.
Upvotes: 4
Reputation: 79
Dynamically Detect Browser Vertical Scrollbar Event by comparing window.innerWidth to getBoundingClientRect() of a DIV element using Javascript. Tested with latest IE FF Chrome. See documentation here
Upvotes: 0
Reputation: 11185
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 :-)
Upvotes: 8
Reputation: 2677
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
Upvotes: 42