[javascript] horizontal scrollbar on top and bottom of table

I've a very large table on my page. So I decided to put a horizontal scrollbar on the bottom of the table. But I would like this scrollbar to be also on top on the table.

What I have in the template is this:

<div style="overflow:auto; width:100%; height:130%">
<table id="data" style="width:100%">...</table>
</div>

Is this possible to do in HTML and CSS only?

This question is related to javascript jquery html css

The answer is


Try using the jquery.doubleScroll plugin :

jQuery :

$(document).ready(function(){
  $('#double-scroll').doubleScroll();
});

CSS :

#double-scroll{
  width: 400px;
}

HTML :

<div id="double-scroll">
  <table id="very-wide-element">
    <tbody>
      <tr>
        <td></td>
      </tr>
    </tbody>
  </table>
</div>

Angular version

I combined 2 answers here. (@simo and @bresleveloper)

https://stackblitz.com/edit/angular-double-scroll?file=src%2Fapp%2Fapp.component.html

inline-component

@Component({
  selector: 'app-double-scroll',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="wrapper1" #wrapper1>
      <div class="div1" #div1></div>
    </div>
    <div class="wrapper2" #wrapper2>
        <div class="div2" #div2>
            <ng-content></ng-content>
        </div>
    </div>
  `,
  styles: [
    `
      .wrapper1, .wrapper2 { width: 100%; overflow-x: auto; overflow-y: hidden; }
    `,
    `
      .div1 { overflow: hidden; height: 0.5px;}
    `,
    `
      .div2 { overflow: hidden; min-width: min-content}
    `
  ]
})
export class DoubleScrollComponent implements AfterViewInit {

  @ViewChild('wrapper1') wrapper1: ElementRef<any>;
  @ViewChild('wrapper2') wrapper2: ElementRef<any>;

  @ViewChild('div1') div1: ElementRef<any>;
  @ViewChild('div2') div2: ElementRef<any>;

  constructor(private _r: Renderer2, private _cd: ChangeDetectorRef) {
  }


  ngAfterViewInit() {

    this._cd.detach();

    this._r.setStyle(this.div1.nativeElement, 'width', this.div2.nativeElement.clientWidth + 'px' );

    this.wrapper1.nativeElement.onscroll = e => this.wrapper2.nativeElement.scroll((e.target as HTMLElement).scrollLeft, 0)
    this.wrapper2.nativeElement.onscroll = e => this.wrapper1.nativeElement.scroll((e.target as HTMLElement).scrollLeft, 0)

  }

}

example

<div style="width: 200px; border: 1px black dashed">

  <app-double-scroll>
    <div style="min-width: 400px; background-color: red; word-break: keep-all; white-space: nowrap;">
      long ass text long ass text long ass text long ass text long ass text long ass text long ass text long ass text long ass text 
    </div>
  </app-double-scroll>

  <br>
  <hr>
  <br>

  <app-double-scroll>
    <div style="display: inline-block; background-color: green; word-break: keep-all; white-space: nowrap;">
      short ass text
    </div>
  </app-double-scroll>

  <br>
  <hr>
  <br>

  <app-double-scroll>
    <table width="100%" border="0" cellspacing="0" cellpadding="0">
      <tbody>
        <tr>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
          <td>table cell</td>
        </tr>
      </tbody>
    </table>
  </app-double-scroll>

</div>

StanleyH's answer was excellent, but it had one unfortunate bug: clicking the shaded area of the scrollbar no longer jumps to the selection you click. Instead, what you get is a very small and somewhat annoying increment in the position of the scrollbar.

Tested: 4 versions of Firefox (100% affected), 4 versions of Chrome (50% affected).

Here's my jsfiddle. You can get around this with by having an on/off (true/false) var that allows only one onScroll() event to trigger at a time:

var scrolling = false;
$(".wrapper1").scroll(function(){
    if(scrolling) {
      scrolling = false;
      return true;
    }
    scrolling = true;
    $(".wrapper2")
        .scrollLeft($(".wrapper1").scrollLeft());
});
$(".wrapper2").scroll(function(){
    if(scrolling) {
      scrolling = false;
      return true;
    }
      scrolling = true;
    $(".wrapper1")
        .scrollLeft($(".wrapper2").scrollLeft());
});

Problem Behavior With Accepted Answer :

Actually Desired Behavior :

So, just why does this happen? If you run through the code, you'll see that wrapper1 calls wrapper2's scrollLeft, and wrapper2 calls wrapper1's scrollLeft, and repeat this infinitely, so, we have an infinite loop problem. Or, rather: the continued scrolling of the user conflicts with wrapperx's call of the scrolling, an event conflict occurs, and the end result is no jumping in the scrollbars.

Hope this helps someone else out!


You can use a jQuery plugin that will do the job for you :

The plugin will handle all the logic for you.


There is an issue with scroll direction: rtl. It seems the plugin doesn't support it correctly. Here is the fiddle: fiddle

Note that it works correctly in Chrome, but in all other popular browsers the top scroll bar direction remains left.


First of all, great answer, @StanleyH. If someone is wondering how to make the double scroll container with dynamic width :

css

.wrapper1, .wrapper2 { width: 100%; overflow-x: scroll; overflow-y: hidden; }
.wrapper1 { height: 20px; }
.div1 { height: 20px; }
.div2 { overflow: none; }

js

$(function () {
    $('.wrapper1').on('scroll', function (e) {
        $('.wrapper2').scrollLeft($('.wrapper1').scrollLeft());
    }); 
    $('.wrapper2').on('scroll', function (e) {
        $('.wrapper1').scrollLeft($('.wrapper2').scrollLeft());
    });
});
$(window).on('load', function (e) {
    $('.div1').width($('table').width());
    $('.div2').width($('table').width());
});

html

<div class="wrapper1">
    <div class="div1"></div>
</div>
<div class="wrapper2">
    <div class="div2">
        <table>
            <tbody>
                <tr>
                    <td>table cell</td>
                    <td>table cell</td>
                    <!-- ... -->
                    <td>table cell</td>
                    <td>table cell</td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

demo

http://jsfiddle.net/simo/67xSL/


As far as I'm aware this isn't possible with HTML and CSS.


Here is an example for VueJS

index.page

<template>
  <div>
    <div ref="topScroll" class="top-scroll" @scroll.passive="handleScroll">
      <div
        :style="{
          width: `${contentWidth}px`,
          height: '12px'
        }"
      />
    </div>
    <div ref="content" class="content" @scroll.passive="handleScroll">
      <div
        :style="{
          width: `${contentWidth}px`
        }"
      >
        Lorem ipsum dolor sit amet, ipsum dictum vulputate molestie id magna,
        nunc laoreet maecenas, molestie ipsum donec lectus ut et sit, aut ut ut
        viverra vivamus mollis in, integer diam purus penatibus. Augue consequat
        quis phasellus non, congue tristique ac arcu cras ligula congue, elit
        hendrerit lectus faucibus arcu ligula a, id hendrerit dolor nec nec
        placerat. Vel ornare tincidunt tincidunt, erat amet mollis quisque, odio
        cursus gravida libero aliquam duis, dolor sed nulla dignissim praesent
        erat, voluptatem pede aliquam. Ut et tellus mi fermentum varius, feugiat
        nullam nunc ultrices, ullamcorper pede, nunc vestibulum, scelerisque
        nunc lectus integer. Nec id scelerisque vestibulum, elit sit, cursus
        neque varius. Fusce in, nunc donec, volutpat mauris wisi sem, non
        sapien. Pellentesque nisl, lectus eros hendrerit dui. In metus aptent
        consectetuer, sociosqu massa mus fermentum mauris dis, donec erat nunc
        orci.
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      contentWidth: 1000
    }
  },
  methods: {
    handleScroll(event) {
      if (event.target._prevClass === 'content') {
        this.$refs.topScroll.scrollLeft = this.$refs.content.scrollLeft
      } else {
        this.$refs.content.scrollLeft = this.$refs.topScroll.scrollLeft
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.top-scroll,
.content {
  overflow: auto;
  max-width: 100%;
}
.top-scroll {
  margin-top: 50px;
}
</style>

TAKE NOTE on the height: 12px.. I made it 12px so it will be noticed on Mac Users as well. But w/ windows, 0.1px is good enough


In vanilla Javascript/Angular you can do this like this:

scroll() {
    let scroller = document.querySelector('.above-scroller');
    let table = document.querySelector('.table');
    table.scrollTo(scroller.scrollLeft,0);
  }

HTML:

<div class="above-scroller" (scroll)="scroll()">
  <div class="scroller"></div>
</div>
<div class="table" >
  <table></table>
</div>

CSS:

.above-scroller  {
   overflow-x: scroll;
   overflow-y:hidden;
   height: 20px;
   width: 1200px
 }

.scroller {
  width:4500px;
  height: 20px;
}

.table {
  width:100%;
  height: 100%;
  overflow: auto;
}

a javascript only solution that's based on @HoldOffHunger and @bobince answers

<div id="doublescroll">

.

function DoubleScroll(element) {
            var scrollbar= document.createElement('div');
            scrollbar.appendChild(document.createElement('div'));
            scrollbar.style.overflow= 'auto';
            scrollbar.style.overflowY= 'hidden';
            scrollbar.firstChild.style.width= element.scrollWidth+'px';
            scrollbar.firstChild.style.paddingTop= '1px';
            scrollbar.firstChild.appendChild(document.createTextNode('\xA0'));
            var running = false;
            scrollbar.onscroll= function() {
                if(running) {
                    running = false;
                    return;
                }
                running = true;
                element.scrollLeft= scrollbar.scrollLeft;
            };
            element.onscroll= function() {
                if(running) {
                    running = false;
                    return;
                }
                running = true;
                scrollbar.scrollLeft= element.scrollLeft;
            };
            element.parentNode.insertBefore(scrollbar, element);
        }

    DoubleScroll(document.getElementById('doublescroll'));

Based on @StanleyH solution I created an AngularJS directive, demo on jsFiddle.

Easy to use:

<div data-double-scroll-bar-horizontal> {{content}} or static content </div>

For AngularJS developers


Linking the scrollers worked, but in the way it's written it creates a loop which makes scrolling slow in most browsers if you click on the part of the lighter scrollbar and hold it (not when dragging the scroller).

I fixed it with a flag:

$(function() {
    x = 1;
    $(".wrapper1").scroll(function() {
        if (x == 1) {
            x = 0;
            $(".wrapper2")
                .scrollLeft($(".wrapper1").scrollLeft());
        } else {
            x = 1;
        }
    });


    $(".wrapper2").scroll(function() {
        if (x == 1) {
            x = 0;
            $(".wrapper1")
                .scrollLeft($(".wrapper2").scrollLeft());
        } else {
            x = 1;
        }
    });
});

There is one way to achieve this that I did not see anybody mentioning here.

By rotating the parent container by 180 degrees and the child-container again by 180 degrees the scrollbar will be shown at top

.parent {
  transform: rotateX(180deg);
  overflow-x: auto;
} 
.child {
  transform: rotateX(180deg);
}

For reference see the issue in the w3c repository.


An AngularJs directive for achieving this: To use it, add css class double-hscroll to your element. You will need jQuery and AngularJs for this.

import angular from 'angular';

var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, $compile) {
  $scope.name = 'Dual wielded horizontal scroller';
});

app.directive('doubleHscroll', function($compile) {
  return {
restrict: 'C',
link: function(scope, elem, attr){

  var elemWidth = parseInt(elem[0].clientWidth);

  elem.wrap(`<div id='wrapscroll' style='width:${elemWidth}px;overflow:scroll'></div>`); 
  //note the top scroll contains an empty space as a 'trick' 
  $('#wrapscroll').before(`<div id='topscroll' style='height:20px; overflow:scroll;width:${elemWidth}px'><div style='min-width:${elemWidth}px'> </div></div>`);

  $(function(){
    $('#topscroll').scroll(function(){
      $("#wrapscroll").scrollLeft($("#topscroll").scrollLeft());
    });
    $('#wrapscroll').scroll(function() {
      $("#topscroll").scrollLeft($("#wrapscroll").scrollLeft());
    });

  });  

}

  };


});

Expanding on StanleyH's answer, and trying to find the minimum required, here is what I implemented:

JavaScript (called once from somewhere like $(document).ready()):

function doubleScroll(){
        $(".topScrollVisible").scroll(function(){
            $(".tableWrapper")
                .scrollLeft($(".topScrollVisible").scrollLeft());
        });
        $(".tableWrapper").scroll(function(){
            $(".topScrollVisible")
                .scrollLeft($(".tableWrapper").scrollLeft());
        });
}

HTML (note that the widths will change the scroll bar length):

<div class="topScrollVisible" style="overflow-x:scroll">
    <div class="topScrollTableLength" style="width:1520px; height:20px">
    </div>
</div>
<div class="tableWrapper" style="overflow:auto; height:100%;">
    <table id="myTable" style="width:1470px" class="myTableClass">
...
    </table>

That's it.


If you are using iscroll.js on webkit browser or mobile browser, you could try:

$('#pageWrapper>div:last-child').css('top', "0px");

to all angular/nativeJs fans, implementing @simo's answer

HTML (no change)

<div class="top-scroll-wrapper">
    <div class="top-scroll"></div>
</div>

CSS (no change, width: 90% is my desing)

.top-scroll-wrapper { width: 90%;height: 20px;margin: auto;padding: 0 16px;overflow-x: auto;overflow-y: hidden;}
.top-scroll { height: 20px; }

JS (like onload) or ngAfterViewChecked (all the as are for TypeScript)

let $topscroll = document.querySelector(".top-scroll") as HTMLElement
let $topscrollWrapper = document.querySelector(".top-scroll-wrapper") as HTMLElement
let $table = document.querySelectorAll('mat-card')[3] as HTMLElement

$topscroll.style.width = totalWidth + 'px'
$topscrollWrapper.onscroll = e => $table.scroll((e.target as HTMLElement).scrollLeft, 0)
$table.onscroll = e => $topscrollWrapper.scroll((e.target as HTMLElement).scrollLeft, 0)

Without JQuery (2017)

Because you might not need JQuery, here is a working Vanilla JS version based on @StanleyH answer:

_x000D_
_x000D_
var wrapper1 = document.getElementById('wrapper1');_x000D_
var wrapper2 = document.getElementById('wrapper2');_x000D_
wrapper1.onscroll = function() {_x000D_
  wrapper2.scrollLeft = wrapper1.scrollLeft;_x000D_
};_x000D_
wrapper2.onscroll = function() {_x000D_
  wrapper1.scrollLeft = wrapper2.scrollLeft;_x000D_
};
_x000D_
#wrapper1, #wrapper2{width: 300px; border: none 0px RED;_x000D_
overflow-x: scroll; overflow-y:hidden;}_x000D_
#wrapper1{height: 20px; }_x000D_
#wrapper2{height: 100px; }_x000D_
#div1 {width:1000px; height: 20px; }_x000D_
#div2 {width:1000px; height: 100px; background-color: #88FF88;_x000D_
overflow: auto;}
_x000D_
<div id="wrapper1">_x000D_
    <div id="div1">_x000D_
    </div>_x000D_
</div>_x000D_
<div id="wrapper2">_x000D_
    <div id="div2">_x000D_
    aaaa bbbb cccc dddd aaaa bbbb cccc _x000D_
    dddd aaaa bbbb cccc dddd aaaa bbbb _x000D_
    cccc dddd aaaa bbbb cccc dddd aaaa _x000D_
    bbbb cccc dddd aaaa bbbb cccc dddd_x000D_
    </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_


Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to jquery

How to make a variable accessible outside a function? Jquery assiging class to th in a table Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Getting all files in directory with ajax Bootstrap 4 multiselect dropdown Cross-Origin Read Blocking (CORB) bootstrap 4 file input doesn't show the file name Jquery AJAX: No 'Access-Control-Allow-Origin' header is present on the requested resource how to remove json object key and value.?

Examples related to html

Embed ruby within URL : Middleman Blog Please help me convert this script to a simple image slider Generating a list of pages (not posts) without the index file Why there is this "clear" class before footer? Is it possible to change the content HTML5 alert messages? Getting all files in directory with ajax DevTools failed to load SourceMap: Could not load content for chrome-extension How to set width of mat-table column in angular? How to open a link in new tab using angular? ERROR Error: Uncaught (in promise), Cannot match any routes. URL Segment

Examples related to css

need to add a class to an element Using Lato fonts in my css (@font-face) Please help me convert this script to a simple image slider Why there is this "clear" class before footer? How to set width of mat-table column in angular? Center content vertically on Vuetify bootstrap 4 file input doesn't show the file name Bootstrap 4: responsive sidebar menu to top navbar Stylesheet not loaded because of MIME-type Force flex item to span full row width