34

I'm working on a custom knockout binding that determines if a particular element is being scrolled, and updates the bound observable with the element's top relative to the viewport. Right now, the binding seems to work, but I have some worries about whether there are some circumstances where it won't.

HTML:

Scroll position: <span data-bind="text: scrollPosition"></span>

<div class="longdiv">    
    <p data-bind="scroll: scrollPosition">This is some text.</p>
    <div class="shim"></div>
</div>

CSS:

.longdiv {
    width: 200px;
    height: 200px;
    overflow: scroll;
    border: 1px solid black;
}

JS:

ko.bindingHandlers.scroll = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var firstScrollableContainer = null;

        var binding = allBindings.get('scroll');

        $(element).parents().each(function (i, parent) {
            if ($(parent).css('overflow')=='scroll') {
                firstScrollableContainer = parent;
                return false;
            }
        });

        firstScrollableContainer = firstScrollableContainer || window;                

        binding(element.getBoundingClientRect().top);

        $(firstScrollableContainer).scroll(function() {            
            binding(element.getBoundingClientRect().top);
        });
    }
};

var ViewModel = function() {
    var self = this;

    self.scrollPosition = ko.observable(0);
};

ko.applyBindings(new ViewModel());

JSFiddle

The binding takes the element and uses jQuery to walk up the parent chain looking to see if the parent element has overflow: scroll set. If it finds a div with overflow: scroll, it binds an event handler to that element's scroll event. If it doesn't find a parent with overflow: scroll, it then binds to the scroll event of the window.

So what I'm looking for, given a document structured like so:

body > div > div > div > p

is the containing element closest to p that can be scrolled, so that I can attach an event handler to it.

My question is: is looking at overflow: scroll a sufficient test to see if a parent element can be scrolled? If not, what should I be looking at?

EDIT: Based on your helpful comments and answers, here is the solution I came up with:

function scrollable(element) {
    var vertically_scrollable, horizontally_scrollable;

    var e = $(element);

     if (   e.css('overflow') == 'scroll' 
         || e.css('overflow') == 'auto'
         || e.css('overflowY') == 'scroll'
         || e.css('overflowY') == 'auto'
         || e.css('height') != 'none'
         || e.css('max-height') != 'none'                          
         ) {
         return true;
    } else {
        return false;
    }
}
Flip
  • 6,233
  • 7
  • 46
  • 75
McCroskey
  • 1,091
  • 1
  • 11
  • 21
  • 1
    I tend to think that verifying `overflow`, `overflow-x` and `overflow-y` should be enough, but I'm not sure. Check this: http://jsperf.com/jquery-scrollable – Alejandro Iván Apr 29 '15 at 23:38
  • A comment: you can use jQuery's `scrollTop()` to get the scroll position instead of `getBoundingClientRect` if you want. – Michael Best Apr 30 '15 at 20:55

5 Answers5

26

Do you want to know if an element can ever scroll or can currently scroll?

Can an element ever scroll?

An element can scroll if it has a fixed height (or max-height) and overflow-y is scroll or auto. But since it's not easy to tell if an element's height is fixed or not, it's probably sufficient to just check overflow-y:

e.css('overflow-y') == 'scroll' || e.css('overflow-y') == 'auto'

Can an element scroll right now?

An element can scroll right now if its scrollHeight is greater than its clientHeight and if it has a scrollbar, which can be determined by comparing clientWidth and offsetWidth (taking margins and borders into account) or checking if overflow-y is scroll or auto.

Community
  • 1
  • 1
Michael Best
  • 16,623
  • 1
  • 37
  • 70
  • Specifically, I want to know if it can ever scroll, so that I can attach an event handler to it if it does. – McCroskey Apr 30 '15 at 16:19
  • It turns out that it's hard to figure out if an element has a fixed height. `e.css('height')` will always return a pixel value. – Michael Best Apr 30 '15 at 20:51
22

This is probably the safest solution (jQuery required, for plain JavaScript see below):

$.fn.isHScrollable = function () {
    return this[0].scrollWidth > this[0].clientWidth;
};

$.fn.isVScrollable = function () {
    return this[0].scrollHeight > this[0].clientHeight;
};

$.fn.isScrollable = function () {
    return this[0].scrollWidth > this[0].clientWidth || this[0].scrollHeight > this[0].clientHeight;
};

Then you can check if an element is scrollable like this:

$(parent).isScrollable();

For usage without jQuery you can implement functions like this:

function isScrollable(element) {
    return element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
};

var myParent = document.getElementById('myParent')
isScrollable(myParent)

RafaelKr
  • 915
  • 8
  • 18
  • 4
    This solution can return `true` for elements that have `overflow:hidden` or `overflow:visible` → those cases can have a scrollHeight > clientHeight without being scrollable – Philipp Jun 27 '21 at 19:39
5

Merging the two answers together, and adding a little something of my own, this is what I use to check Vertical scrolling. It can be easily converted for other cases. (H & VH)

function isScrollable(e){
    if( e.scrollTopMax !== undefined )
        return e.scrollTopMax > 0; //All Hail Firefox and it's superior technology!

    if( e == document.scrollingElement ) //If what you're checking is BODY (or HTML depending on your css styles)
        return e.scrollHeight > e.clientHeight; //This is a special case.

    return e.scrollHeight > e.clientHeight && ["scroll", "auto"].indexOf(getComputedStyle(e).overflowY) >= 0



}

I tested this on Firefox, and Chromium. Both Linux. you might still wanna check them for yourself though.

aliqandil
  • 1,673
  • 18
  • 28
  • The additional check for overflow (x/y) is not required as this is automatically determined with the previous checks. – RafaelKr Aug 26 '19 at 13:37
  • 1
    There are elements that are not marked to be scrollable, they are simply overflown out of their parents box and not meant to be scrolled. if you omit the part you check the style, it means you consider those actually scrollable. besides, that's not how the Firefox's scrollTopMax works, so omitting it will also introduce inconsistent behaviour. – aliqandil Aug 26 '19 at 13:47
  • not sure how this would work on mobile, which has scrolling elements without a visible scroll bar – Garr Godfrey Feb 06 '21 at 03:12
  • a visible scroll bar is just that, a visual element. shouldn't effect anything about the code. (Also desktop browsers can also hide their scrollbar) – aliqandil Feb 06 '21 at 14:03
3

Ran into this solution, seemed cleaner than anything already in this thread. Confirmed to work for my scroll check needs.

const isScrollable = function (ele) {
    // Compare the height to see if the element has scrollable content
    const hasScrollableContent = ele.scrollHeight > ele.clientHeight;

    // It's not enough because the element's `overflow-y` style can be set as
    // * `hidden`
    // * `hidden !important`
    // In those cases, the scrollbar isn't shown
    const overflowYStyle = window.getComputedStyle(ele).overflowY;
    const isOverflowHidden = overflowYStyle.indexOf('hidden') !== -1;

    return hasScrollableContent && !isOverflowHidden;
};
Llama D'Attore
  • 323
  • 1
  • 12
1

Although this is an old question, I will post my own (latest) answer that works correctly, after various failed attempts to determine if an element is actually scrollable.

function canScroll(el, scrollAxis) {
    if (0 === el[scrollAxis]) {
        el[scrollAxis] = 1;
        if (1 === el[scrollAxis]) {
            el[scrollAxis] = 0;
            return true;
        }
    } else {
        return true;
    }
    return false;
}

function isScrollableX(el) {
    return (el.scrollWidth > el.clientWidth) && canScroll(el, 'scrollLeft') && ('hidden' !== getComputedStyle(el).overflowX);
}

function isScrollableY(el) {
    return (el.scrollHeight > el.clientHeight) && canScroll(el, 'scrollTop') && ('hidden' !== getComputedStyle(el).overflowY);
}

function isScrollable(el) {
    return isScrollableX(el) || isScrollableY(el);
}

Use:

console.log(isScrollable(document.querySelector('#myElement')));
Nikos M.
  • 8,033
  • 4
  • 36
  • 43