Sticky scrollbar at bottom of table
Asked Answered
P

5

30

I'm not sure if "sticky" is the term for this, but is there a way to make the scrollbar from overflow:auto stay visible?

I have a rather large table that I want to be scrollable horizontally; however, the table is fairly tall as well, so when the page loads the horizontal scrollbar is not within the viewport of the browser, so it's rather hard to tell that the table is scrollable at all.

<div style = 'width:900px;overflow:auto'>
    <table>
        <!-- Very large table here -->
    </table>
</div>

The scroll bar appears below the table, but unfortunately the table is so tall you can't see it unless you scroll down.

I'd like to have the horizontal scrollbar stay visible even if the table goes off the screen, maybe fixed to the bottom of the viewport. Ideally I'd like to do it using only CSS or a minimal amount of javascript.

Pozzuoli answered 21/4, 2014 at 22:9 Comment(3)
This is very nicely done and I think it suits you since it's a possible solutions, worth checking this out: #3934771Dimidiate
It looks like it would work, but two horizontal scrollbars seems a bit awkward. Still, if there's no simple way to do a sticky scrollbar I guess I'll resort to that.Pozzuoli
I updated the fiddle from that other question to be more uber and more about this question: jsfiddle.net/TBnqw/2283Salaam
S
15

Here is a script for that http://jsfiddle.net/TBnqw/2288/

$(function($){
    var scrollbar = $('<div id="fixed-scrollbar"><div></div></div>').appendTo($(document.body));
    scrollbar.hide().css({
        overflowX:'auto',
        position:'fixed',
        width:'100%',
        bottom:0
    });
    var fakecontent = scrollbar.find('div');

    function top(e) {
        return e.offset().top;
    }

    function bottom(e) {
        return e.offset().top + e.height();
    }

    var active = $([]);
    function find_active() {
        scrollbar.show();
        var active = $([]);
        $('.fixed-scrollbar').each(function() {
            if (top($(this)) < top(scrollbar) && bottom($(this)) > bottom(scrollbar)) {
                fakecontent.width($(this).get(0).scrollWidth);
                fakecontent.height(1);
                active = $(this);
            }
        });
        fit(active);
        return active;
    }

    function fit(active) {
        if (!active.length) return scrollbar.hide();
        scrollbar.css({left: active.offset().left, width:active.width()});
        fakecontent.width($(this).get(0).scrollWidth);
        fakecontent.height(1);
        delete lastScroll;
    }

    function onscroll(){
        var oldactive = active;
        active = find_active();
        if (oldactive.not(active).length) {
            oldactive.unbind('scroll', update);
        }
        if (active.not(oldactive).length) {
            active.scroll(update);
        }
        update();
    }

    var lastScroll;
    function scroll() {
        if (!active.length) return;
        if (scrollbar.scrollLeft() === lastScroll) return;
        lastScroll = scrollbar.scrollLeft();
        active.scrollLeft(lastScroll);
    }

    function update() {
        if (!active.length) return;
        if (active.scrollLeft() === lastScroll) return;
        lastScroll = active.scrollLeft();
        scrollbar.scrollLeft(lastScroll);
    }

    scrollbar.scroll(scroll);

    onscroll();
    $(window).scroll(onscroll);
    $(window).resize(onscroll);
});

It is a quick test rather than a complete generic plugin, but is a good start, I think

Salaam answered 25/7, 2014 at 8:51 Comment(2)
If we have a vertical scrollbar overflow-y:auto, your scrollbar will not scroll to the end.Myer
You need scrollbar.style.width = container.clientWidth + "px"Myer
P
7

Here's my take, @user2451227's is almost perfect, but didn't work with nested overflowed elements and had a number of performance issues, so I rewrote it:

$(function($){
    var fixedBarTemplate = '<div class="fixed-scrollbar"><div></div></div>';
    var fixedBarCSS = { display: 'none', overflowX: 'scroll', position: 'fixed',  width: '100%', bottom: 0 };

    $('.fixed-scrollbar-container').each(function() {
        var $container = $(this);
        var $bar = $(fixedBarTemplate).appendTo($container).css(fixedBarCSS);

        $bar.scroll(function() {
            $container.scrollLeft($bar.scrollLeft());
        });

        $bar.data("status", "off");
    });

    var fixSize = function() {
        $('.fixed-scrollbar').each(function() {
            var $bar = $(this);
            var $container = $bar.parent();

            $bar.children('div').height(1).width($container[0].scrollWidth);
            $bar.width($container.width()).scrollLeft($container.scrollLeft());
        });

        $(window).trigger("scroll.fixedbar");
    };

    $(window).on("load.fixedbar resize.fixedbar", function() {
        fixSize();
    });

    var scrollTimeout = null;

    $(window).on("scroll.fixedbar", function() { 
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(function() {
            $('.fixed-scrollbar-container').each(function() {
                var $container = $(this);
                var $bar = $container.children('.fixed-scrollbar');

                if($bar.length && ($container[0].scrollWidth > $container.width())) {
                    var containerOffset = {top: $container.offset().top, bottom: $container.offset().top + $container.height() };
                    var windowOffset = {top: $(window).scrollTop(), bottom: $(window).scrollTop() + $(window).height() };

                    if((containerOffset.top > windowOffset.bottom) || (windowOffset.bottom > containerOffset.bottom)) {
                        if($bar.data("status") == "on") {
                            $bar.hide().data("status", "off");
                        }
                    } else {
                        if($bar.data("status") == "off") {
                            $bar.show().data("status", "on");
                            $bar.scrollLeft($container.scrollLeft());
                        }
                    }
                } else {
                    if($bar.data("status") == "on") {
                        $bar.hide().data("status", "off");
                    }
                }
            });
        }, 50);
    });

    $(window).trigger("scroll.fixedbar");
});

Usage: Add the class fixed-scrollbar-container to your horizontally overflowed element, then include this code. If the container is updated or changes in size, run $(window).trigger("resize.fixedbar"); to update the bar.

Demo: http://jsfiddle.net/8zoks7wz/1/

Publicize answered 10/11, 2015 at 0:49 Comment(1)
The "fake" scrollbar is not reflecting the real position when scrolled by gestures but the solution from @Salaam does.Myer
J
2

@Mahn - I made a small update to the following function:

$('.fixed-scrollbar-container').each(function() {

    var container = jQuery(this);

    if (container[0].offsetWidth < container[0].scrollWidth) {
        var bar = jQuery(fixedBarTemplate).appendTo(container).css(fixedBarCSS);

        bar.scroll(function() {
            container.scrollLeft(bar.scrollLeft());
        });

        bar.data("status", "off");    
    }
});

The if statement looks if the container offsetWidth is smaller than the scrollWidth. Else you will also get a fixed scrollbar if the content happens to be smaller than the container. I did not like having a disfunctional scrollbar, hence this edit.

Juliajulian answered 5/1, 2016 at 11:31 Comment(2)
Hi, welcome to Stack Overflow. If this is an update to another answer, you should post it as a comment and not as an independent answer. Keep up the good work, cheers.Stochmal
@Stochmal - I do not have enough rep to do that yet ... I can only make new posts and comment on my own posts.Juliajulian
F
1

How about restricting the height of the containing div so it stays within the body? You could then have the table scroll within that div.

Working jsfiddle here: http://jsfiddle.net/fybLK/

html, body {height: 100%; margin: 0; padding: 0;}
div {
    width:500px;
    max-height: 100%;
    overflow:auto;
    background: steelblue;}
table {
    width: 1000px;
    height: 1000px;
    color: #fff;}

Here, I've set the html and body to 100% height so that the containing div can be sized.

Farrar answered 21/4, 2014 at 22:50 Comment(1)
It's a good option, but I'd prefer not having to add a vertical scrollbar if at all possible. That being said, this solution is really simple and doesn't look too bad, so I might end up going with this.Pozzuoli
T
1

This is the sticky scrollbar version with sticky top and left headers, and inside overflow container.
Use .left-header, .top-left-header for column th's to make them sticky to the left.
Fire the pageUpdate event after table updates to recalculate sticky headers.
Horizontal header stickiness can be achieved by pure css inside the container with overflow:clip.

$(function ($) {
const SELECTOR = '.responsive-container-js';
const stickyBarTemplate = '<div class="sticky-scrollbar"><div></div></div>';
const stickyBarCSS = {
    display: 'none',
    overflowX: 'scroll',
    position: 'sticky',
    width: '100%',
    bottom: 0,
    'z-index': 999
};

renderBars();
$(window).on('load pageUpdate', renderBars);

function renderBars() {
    $(SELECTOR).each(function () {
        let $container = $(this);
        let $table = $container.find("table:first");
        let $bar = $container.find('.sticky-scrollbar');
        if (!$bar.length) {
            $bar = $(stickyBarTemplate).appendTo($container).css(stickyBarCSS);

            const resizeObserver = new ResizeObserver(entries => {
                updateBar()
            });
            resizeObserver.observe($container[0]);
            resizeObserver.observe($table[0]);

            updateBar();
            function updateBar() {
                $bar.children('div').height(1).width($table.width());
                $bar.width($container.width()).scrollLeft($container.scrollLeft());

                requestAnimationFrame(function () {
                    if ($table.width() > $container.width()) {
                        $bar.show();

                        $bar.scroll(function () {
                            $table
                                .css({"transform": "translateX(-" + $bar.scrollLeft() + "px)"});
                            $container
                                .find('.left-header, .top-left-header')
                                .css({"transform": "translateX(" + $bar.scrollLeft() + "px)"});
                        });

                        $bar.scrollLeft($container.scrollLeft());
                    } else {
                        $bar.hide();
                    }
                });
            }
        }
    });
}
});
.responsive-container-js {
  overflow: clip;
}
.top-left-header {
  left:0;
  top:0;
  z-index:101;
}

table {
  white-space: nowrap;
}
table th {
  height: 4em;
  border: 1px solid silver;
  padding: .5em;
  position:sticky;
  top:0;
  background: #eee;
  z-index: 99;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.7/jquery.min.js"></script>

<p>More content here</p>
<p>More content here</p>
<p>More content here</p>

<div class="responsive-container-js">
<table>
    <tr>
        <th class="top-left-header">Top Left Header</th>
        <th>Top Header 2</th>
        <th>Top Header 3</th>
        <th>Top Header 4</th>
        <th>Top Header 5</th>
        <th>Top Header 6</th>
        <th>Top Header 7</th>
        <th>Top Header 8</th>
        <th>Top Header 9</th>
        <th>Top Header 10</th>
        <th>Top Header 11</th>
        <th>Top Header 12</th>
    </tr>
        <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    <tr>
        <th class="left-header">Left header 1</th>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
        <td>Data</td>
    </tr>
    
</table>
</div>

<p>More content here</p>
<p>More content here</p>
<p>More content here</p>
<p>More content here</p>
Tsarism answered 18/4, 2023 at 10:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.