just another cross browser custom scrollbars
Cross browser custom scrollbars
Many people might ask themselves: “Why..?”
Others might think: “That is soo ugly” or “Don’t change the systems’s native elements.”
But let me explain
We are living in a time where webapps have grown in popularity. Not only that but also has it gotten more and more important to customize each little bit of a webapp.
I for myself really like those overall styled apps as long as it doesn’t affect the performance and basic usage.
There are “a lot” of plugins that replace scrollbars, checkboxes, radiobuttons, selects etc…
Checkboxes and radiobuttons can be nicely styled with pure CSS in most browsers (IE8 and lower fail here)
Selects are very hard to style so a plugin is needed. There are quite a few good ones out there.
So what about Scrollbars?
I guess if there’s anything that really has too many plugins it’s scrollbars.
Here’s a list of a few popular ones:
nanoscroller
nicescroll
Tiny Scrollbar
antiscroll
jsScrollPane
So.. why did I decide to write another one?
One reason is, that I actually always wanted to write a scrollbar plugin because I really like writing JavaScripts where interactions are translated 😉
But the main reason is because of all plugins that I’ve seen, I haven’t found one that does it the way I think is “the cool” way.
Most Plugins use the mousewheel.js by Brandon Aaron. They listen to the mousewheel events and then translate them to a div’s top
CSS-vaue.
Translate = fun? … Yeah.. but not like that.
If I scroll a div I could simply listen to the scroll instead. This removes a dependency and also give a way better native feeling.
Gives a feeling? No, even better it IS native.
So what we will do is allow native scrolling, listen to the event and translate it to a scrollbar that is dynamically created by our script.
Translate = fun? Yeah… like that it is!
The concept
The main concept is to wrap the content to be scrolled in a div that actually does the scrolling for us. This will allow to have full flexibility when styling or interacting with our content.
This div is wrapped in another div which is just a bit slimmer than the scrolling-area. This way if we add overflow: hidden;
to it, it will hide the original scrollbar. Now we can add our custom scrollbar which will be moved by the listener or provide listeners that will be passed to the content. Instead of moving a static content by it’s position we make use of as many native features as make sense.
The approach
We will write a plugin to modify our DOM and the style our elements with CSS (CSS3 in this case).
So let’s set up a basic plugin frame:
The code
(function ($) { $.fn.customScrollBar = function (options) { // make sure to handle each call separately $(this).each(function (){ // set some default options var defaults = {}; // extend the options options = $.extend(defaults, options); }); } });
and then let’s add some options.
We will allow to add a theme-name, add optional triggers (top-down-arrows) and some events that we can listen for (e.g. scrollstart or scrollend)
(function ($) { $.fn.customScrollBar = function (options) { // set some default options $(this).each(function (){ var defaults = { theme: 'custom-scroll-bar', arrows: true, init: function(e,ui){ // initialized scrollbar }, scrollstarted: function(e,ui){ // the scroll has started }, scrollended: function(e,ui){ // the scroll has ended }, thumbclick: function(e,ui){ // the thumb was clicked } }; // extend the options options = $.extend(defaults, options); }); } });
Ok now we can call our plugin on a jQuery-object.
We will need to set some variables. For one reason to cache some elements and others for our calculations and interactions.
// set the variables var thisElement = $(this); var $body = $('body'); var $window = $(window); // for later use var clickY = 0; var $dragging = null; var scrollTriggerFunction; var deltaY; var clickYY; var scrolling; var showArrows = ""; var scrollEnded;
The next step will be to wrap our element with our two divs
// wrap our element thisElement.wrap('<div class=\"scroll-wrapper\" >'); thisElement.wrap('<div class=\"scroll-area\" >'); </div></div>
More variables. These ones need to be called after we wrapped our element.
// get new elements and dependent calculations var $scrollArea = thisElement.parent(); var $scrollWrapper = $scrollArea.parent(); var thisHeight = parseInt((thisElement.outerHeight()),10); var scrollAreaHeight = parseInt(($scrollArea.outerHeight()),10); var thisScroll = parseInt(($scrollArea.scrollTop()),10);
To calculate the correct offset we need to find out the factor between our div’s height
and the maximum-height
of the wrapper
// the factor will be used for calculating the relation between // our events and elements var factor = thisHeight / scrollAreaHeight; var scrollBarHeight = parseInt((scrollAreaHeight / factor),10);
Now we can read out a few options and create our scrollbar (if it is needed, similar to overflow: auto
)
if (options.arrows) { showArrows = '<span class=\"scroll-trigger top\"></span><span class=\"scroll-trigger bottom\"></span>'; } var newScrollBar = '<div class=\"scroll-track\">' + '<div class=\"scroll-bar\"></div>' + showArrows + '</div> '; // add the scrollbar if (thisHeight > scrollAreaHeight){ $scrollArea.parent().append(newScrollBar); }
Now that the plugin has modified our DOM we can get the rest of the variables
// get new elements and dependents var $scrollBar = $scrollWrapper.find('.scroll-bar'); var $scrollTrack = $scrollWrapper.find('.scroll-track'); var $scrollTriggerTop = $scrollWrapper.find('.scroll-trigger.top'); var $scrollTriggerBottom = $scrollWrapper.find('.scroll-trigger.bottom'); $scrollTrack.addClass(options.theme); var thisMargin = parseInt(($scrollBar.css('margin-top')),10); thisMargin += parseInt(($scrollBar.css('margin-bottom')),10);
Safety first… Let’s make sure the scrollbar is big enough to be visible
// make sure our scrollbar is visible if (scrollBarHeight < thisMargin){ factor = thisHeight / (scrollAreaHeight - thisMargin); } // make sure our scrollbar is visible if (scrollBarHeight < 20){ scrollBarHeight += thisMargin; }
Now we are ready to give the scroll-bar-thumb a height.
// set the height of the scrollbar $scrollBar.css({ height: scrollBarHeight - thisMargin });
Let’s create a function that can be called when the scroll has ended. This will be our first listener.
var scrollHasEnded = function (){ $scrollTrack.removeClass("scrolling"); options.scrollended(thisElement, $scrollTrack); scrolling = false; };
And a second one when the scrollbar is processed
options.init(thisElement, $scrollTrack);
dirty hacking
Webkit has an issue when selecting the content of a div with overflow: hidden
.
So we will force a scrollLeft to 0. tThis creates a steppy and ugly effect but is only called when the user selects to far to the right. Yet it is needed because otherwise the div will stay in the scrolled position, reveal the “real” scrollbar and hide important content
// dirty hack to prevent webkits drag-scroll $scrollWrapper.on('scroll',function (){ var $this = $(this); $this.scrollLeft(0); });
The rest of the Javascript will be our interactions on the div or the scrollbar
// handling the native mouse scroll $scrollArea.on('scroll', function (){ if (!scrolling) { options.scrollstarted(thisElement, $scrollTrack); } var $this = $(this); thisScroll = parseInt(($this.scrollTop()),10); clearTimeout(scrollEnded); scrollEnded = setTimeout(scrollHasEnded,200); $scrollTrack.addClass("scrolling"); $scrollBar.css({ top: thisScroll / factor }); scrolling = true; }); $scrollTrack.on('mousedown', function (e){ var $this = $(this); var thisOffset = parseInt(($this.offset().top),10); var trackOffset = parseInt(($this.find('.scroll-bar').position().top),10); var trackPosition = $this.find('.scroll-bar').position().top / scrollBarHeight; var correctOffset = e.pageY - thisOffset - trackOffset; $this.addClass('clicked'); options.thumbclick(thisElement, $scrollTrack); // prevent the cursor from changing to text-input e.preventDefault(); // calculate the correct offset clickY = thisOffset + correctOffset; clickYY = thisOffset + thisMargin; if ($( e.target).hasClass('scroll-bar')) { $dragging = $(e.target); } }) // scroll to position if the track is clicked (but prevent // when triggers or the bar is clicked) .on('mousedown', '.scroll-track', function (e){ if (!$( e.target).hasClass('scroll-trigger') && !$( e.target).hasClass('scroll-bar')) { deltaY = e.pageY - clickYY; $scrollArea.stop(true,true).animate({scrollTop: deltaY * factor},1); } }); // scrolling via the triggers (up-down-arrows) // Top arrow $scrollTriggerTop.on('mousedown', function (){ $scrollArea.stop(true,true).animate({scrollTop: "-=" + factor + "px"},factor); scrollTriggerFunction = setInterval(function (){ $scrollArea.stop(true,true).animate({scrollTop: "-=" + factor + "px"},factor); },1); }); // Bottom arrow $scrollTriggerBottom.on('mousedown', function (){ $scrollArea.stop(true,true).animate({scrollTop: "+=" + factor + "px"},factor); scrollTriggerFunction = setInterval(function (){ $scrollArea.stop(true,true).animate({scrollTop: "+=" + factor + "px"},factor); },1); }); // on mouseup or mouseleave we will kill all intervals and set // dragging to null to prevent leaking $body.on('mouseup mouseleave', function (){ clearInterval(scrollTriggerFunction); $dragging = null; $scrollTrack.removeClass("clicked"); }) // on mosemove we will move our scrollbar if dragging is // active (after mousedown on scroll-track) .on('mousemove', function (e){ if ($dragging) { deltaY = e.pageY - clickY ; $scrollArea.stop(true,true).animate({scrollTop: deltaY * factor},factor); } });
our JavaScript to call the plugin
$(function (){ var scrollMe = $('.lionScrollBar'); scrollMe.customScrollBar({ theme: 'lion-scroll-bar', arrows: false, init: function(e,ui){ ui.addClass('scrolling'); setTimeout(function(){ ui.removeClass('scrolling'); },600) } }); });
… and the CSS behind it
.scroll-wrapper { height: 400px; width: 300px; overflow: hidden; position: relative; display: inline-block; margin: 50px 50px 0; } .lionScrollBar { padding: 0; } .scroll-wrapper .scroll-area { height: 100%; width: 100%; overflow: auto; padding-right: 20px; margin-right: 20px; position: relative; } .scroll-track { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } .lion-scroll-bar.scroll-track { position: absolute; top: 0; right: 0; width: 11px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; height: 100%; background: rgba(255,255,255,0); border: 1px solid transparent; border-width: 0 0 0 1px; -webkit-transition-duration: 0ms, 500ms; -webkit-transition-delay: 1000ms, 500ms; -webkit-transition-property: width, opacity; -moz-transition-duration: 0ms, 500ms; -moz-transition-delay: 1000ms, 500ms; -moz-transition-property: width, opacity; transition-duration: 0ms, 500ms; transition-delay: 1000ms, 500ms; transition-property: width, opacity; opacity: 0; overflow: hidden; width: 0; } .lion-scroll-bar.scroll-track.scrolling { opacity: 1; -webkit-transition-duration: 0ms, 50ms; -webkit-transition-delay: 0ms, 0ms; -webkit-transition-property: width, opacity; -moz-transition-duration: 0ms, 50ms; -moz-transition-delay: 0ms, 0ms; -moz-transition-property: width, opacity; transition-duration: 0ms, 50ms; transition-delay: 0ms, 0ms; transition-property: width, opacity; width: 10px; } .lion-scroll-bar.scroll-track.clicked, .lion-scroll-bar.scroll-track:hover { opacity: 1; -webkit-transition-duration: 200ms, 50ms; -webkit-transition-delay: 0ms, 0ms; -webkit-transition-property: width, opacity; -moz-transition-duration: 200ms, 50ms; -moz-transition-delay: 0ms, 0ms; -moz-transition-property: width, opacity; transition-duration: 200ms, 50ms; transition-delay: 0ms, 0ms; width: 15px; background: rgba(255,255,255,0.8); border-color: rgba(0,0,0,0.2); box-shadow: 20px 0 20px -20px rgba(0,0,0,0.2) inset; } .lion-scroll-bar.scroll-track .scroll-bar { position: absolute; z-index: 0; margin: 3px 0; top: 0; right: 2px; width: 7px; height: 14px; background: rgba(0,0,0,0.5); -webkit-border-radius: 7px; -moz-border-radius: 7px; border-radius: 7px; -webkit-transition: width 200ms; -moz-transition: width 200ms; transition: width 200ms; } .lion-scroll-bar.scroll-track.clicked .scroll-bar, .lion-scroll-bar.scroll-track:hover .scroll-bar { -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; background: rgba(0,0,0,0.5); width: 10px; }
Restrictions
At the time of posting this plugin does not allow horizontal scrollbars but as time passes I will add this feature.
Nesting scrollbars causes an issue with the dimensions so for now it will not be possible.
Gregor Adams
I specialize in CSS, HTML and JavaScript. As I am a huge fan and also a student of open-source, I try to help other programmers (especially newcomers) as much as I can. I am also very happy for "true" criticism. So feel free to ask any question you like and I will try to give the best possible answer.