I want to implement a custom scrollbar with a flexible track that expands around the thumb using CSS. I try to achieve something like a gravitational lens effect, as if the thumb (red dot) warps the space around it (see the picture below). The gravitational lens shall move with the thumb.
How can I do that?
First: It's possible to build the scrollbar just with HTML and CSS but for the usage you need Javascript.
You can achieve the look with position
, border
and border-radius
. The track gets the border on the left and the right side, the thumb gets it on all sides with a border-radius: 50%
and additionally a white mask (with the width of the track minus its borders) for hiding the thumb borders inside the track. Of course you need also the red dot. All five elements get a position: absolute
.
<div id="scrollbar">
<div id="track"></div>
<div id="thumb">
<div class="white_mask"></div>
<div id="red_dot"></div>
</div>
</div>
Basic CSS (without dimensions)
#scrollbar {
position: absolute;
}
#track {
position: absolute;
border-right: 3px solid #000;
border-left: 3px solid #000;
}
#thumb {
position: absolute;
border: 1px solid #000;
border-radius: 50%;
background-color: white;
}
.white_mask {
position: absolute;
background-color: white;
}
#red_dot {
position: absolute;
border-radius: 50%;
background-color: #9c0000;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#scrollbar {
position: absolute;
right: 100px;
top: 0;
width: 120px;
height: 100%;
z-index: 10;
}
#track {
position: absolute;
top: 0;
right: 20px;
width: 80px;
height: 100%;
border-right: 12px solid #000;
border-left: 12px solid #000;
}
#thumb {
position: absolute;
top: 0;
right: 0;
width: 120px;
height: 128px;
border: 4px solid #000;
border-top: 8px solid #000;
border-bottom: 8px solid #000;
border-radius: 50%;
background-color: white;
}
.white_mask {
position: absolute;
top: -8px;
right: 28px;
width: 56px;
height: 128px;
background-color: white;
}
.white_mask.big {
top: -16px;
right: 12px;
width: 88px;
height: 144px;
}
#thumb_mask {
position: absolute;
top: -16px;
left: 8px;
width: 96px;
height: 144px;
overflow: hidden;
}
.quarter_border {
position: absolute;
width: 80px;
height: 100px;
border-right: 12px solid #000;
border-left: 12px solid #000;
border-radius: 50%;
background-color: white;
}
.quarter_border.top {
top: -52px;
}
.quarter_border.right {
right: -60px;
}
.quarter_border.bottom {
bottom: -52px;
}
.quarter_border.left {
left: -60px;
}
#red_dot {
position: absolute;
top: 0;
left: 0;
width: 112px;
height: 112px;
border: 8px solid white;
border-radius: 50%;
background-color: #9c0000;
}
<div id="container">
<div id="scrollbar">
<div id="track"></div>
<div id="thumb">
<div class="white_mask"></div>
<div id="red_dot"></div>
</div>
</div>
</div>
Since there are small corners where thumb and track meet each other you can add four additional divs with rounded borders in a masking div with overflow: hidden
so that just a quarter of the four divs is visible. You need also a second white mask under the four divs to hide the corners.
For increasing and decreasing the border-width
you need to define different values for top/bottom border and left/right border. Furthermore the red dot needs a white border for smoothing the inside of the thumb border.
<div id="scrollbar">
<div id="track"></div>
<div id="thumb">
<div class="white_mask"></div>
<div class="white_mask small"></div>
<div id="thumb_mask">
<div class="quarter_border top right"></div>
<div class="quarter_border bottom right"></div>
<div class="quarter_border top left"></div>
<div class="quarter_border bottom left"></div>
</div>
<div id="red_dot"></div>
</div>
</div>
Additional CSS (without dimensions)
#thumb {
...
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
#thumb_mask {
position: absolute;
overflow: hidden;
}
.quarter_border {
position: absolute;
border-right: 3px solid #000;
border-left: 3px solid #000;
border-radius: 50%;
background-color: white;
}
#red_dot{
...
border: 2px solid white;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
#scrollbar {
position: absolute;
right: 100px;
top: 0;
width: 120px;
height: 100%;
z-index: 10;
}
#track {
position: absolute;
top: 0;
right: 20px;
width: 80px;
height: 100%;
border-right: 12px solid #000;
border-left: 12px solid #000;
}
#thumb {
position: absolute;
top: 0;
right: 0;
width: 120px;
height: 128px;
border: 4px solid #000;
border-top: 8px solid #000;
border-bottom: 8px solid #000;
border-radius: 50%;
background-color: white;
}
.white_mask {
position: absolute;
top: -4px;
right: 28px;
width: 56px;
height: 128px;
background-color: white;
}
.white_mask.big {
top: -16px;
right: 12px;
width: 88px;
height: 144px;
}
#thumb_mask {
position: absolute;
top: -16px;
left: 8px;
width: 96px;
height: 144px;
overflow: hidden;
}
.quarter_border {
position: absolute;
width: 80px;
height: 100px;
border-right: 12px solid #000;
border-left: 12px solid #000;
border-radius: 50%;
background-color: white;
}
.quarter_border.top {
top: -52px;
}
.quarter_border.right {
right: -60px;
}
.quarter_border.bottom {
bottom: -52px;
}
.quarter_border.left {
left: -60px;
}
#red_dot {
position: absolute;
top: 0;
left: 0;
width: 112px;
height: 112px;
border: 8px solid white;
border-radius: 50%;
background-color: #9c0000;
}
<div id="container">
<div id="scrollbar">
<div id="track"></div>
<div id="thumb">
<div class="white_mask"></div>
<div class="white_mask big"></div>
<div id="thumb_mask">
<div class="quarter_border top right"></div>
<div class="quarter_border bottom right"></div>
<div class="quarter_border top left"></div>
<div class="quarter_border bottom left"></div>
</div>
<div id="red_dot"></div>
</div>
</div>
</div>
Unfortunately the quarters don't fit perfectly to thumb and track. So it could be a better approach to build the scrollbar as SVG and manipulate its components instead.
There are many possibilities and features to manipulate the scrollbar with different properties like top
, scrollTop
or transform
. I use here for demonstration only top
and some basic features (for example I omitted the adjustment of the scrollposition on resize
).
For getting the scrollbar to work first you need three constants thumb_height
(incl. its borders), scroll_range
(maximum thumb position) and diff_height
(hidden content).
const thumb_height = thumb.getBoundingClientRect().height;
const scroll_range = window.innerHeight - thumb_height;
const diff_height = content.scrollHeight - window.innerHeight
Then you need the scroll function where you calculate the new thumb position and the content scroll and style both elements if the thumb is in the scroll range.
The content scroll is the visible percentage of the initial hidden content. The percentage is calculated with: thumb position divided by its maximum, the scroll range.
For dragging the thumb:
You get the thumb position from the mouse cursor position in the mousedown
event: e.clientY
(corrected by the half of the thumb height).
You also have to make sure that the thumb doesn't disappear when the thumb position gets too small or too big, for example when leaving the container (here the window). Therefor you have to check if the position is lower than 0 or higher than the scroll range and style the elements only if not.
function dragThumb(e) {
const thumb_pos = e.clientY - (thumb_height / 2);
const content_scroll = -diff_height * thumb_pos / scroll_range;
if (thumb_pos >= 0 && thumb_pos <= scroll_range) {
thumb.style.top = thumb_pos + 'px';
content.style.top = content_scroll + 'px';
}
}
For mouse wheel:
You get the thumb position from the actual top
property of the thumb increased or decreased (depending on the mouse wheel direction in the wheel
event: e.deltaY
) by the fixed amount 100
(you can modify the value to scroll faster or slower).
For the styling you additionally have to handle the two extreme conditions because the thump position (calculated with the fixed amount) sometimes doesn't "land" exactly on 0 or the scroll range. You could do this with if
blocks like in dragThumb()
(you would need 3) or you could use ternary operators ? :
like in the following code snippet.
function scrollContent(e) {
const thumb_pos = parseInt(thumb.style.top) + (e.deltaY < 0 ? -100 : 100);
const content_scroll = -diff_height * thumb_pos / scroll_range;
thumb.style.top = thumb_pos < 0 ? 0 : (thumb_pos > scroll_range ? scroll_range : thumb_pos) + 'px';
content.style.top = thumb_pos < 0 ? 0 : (thumb_pos > scroll_range ? -diff_height : content_scroll) + 'px';
}
Last you need an event listener where you call the scroll function.
For the mouse wheel:
document.addEventListener('wheel', scrollContent);
For dragging the thumb:
You need two additional event listeners, one for listening for the mouse move only if the thumb is initially clicked, and one for removing that mouse move listener if the mouse button is released.
thumb.addEventListener('mousedown', function(e) {
e.preventDefault(); // fixed a bug with 'mouseup'
document.addEventListener('mousemove', dragThumb);
});
document.addEventListener('mouseup', function() {
document.removeEventListener('mousemove', dragThumb);
});
Both scroll functions are merged into the function scrollContent()
. The thumb position is calculated in the two event handlers (which call scrollContent()
): dragThumb()
and the anonymous function for the wheel
event.
The three constants are calculated in a reset function resetScroll()
(and therefor defined as ordinary vars with let
) that is called on page load and window resize. That function also calles the scroll function scrollContent()
with the actual thumb position.
Because the event listener for mouseup
is "deaf" if the event happens outside the stack snippet, i converted its anonymous function in a function stopDrag()
that is called as event handler for mouseup
and mouseleave
.
document.addEventListener('DOMContentLoaded', function() {
const thumb = document.getElementById('thumb');
const content = document.getElementById('content');
let thumb_height, scroll_range, diff_height;
function resetScroll() {
thumb_height = thumb.getBoundingClientRect().height;
scroll_range = window.innerHeight - thumb_height;
diff_height = content.scrollHeight - window.innerHeight;
scrollContent(parseInt(thumb.style.top));
}
function scrollContent(thumbPos) {
const newScroll = -diff_height * thumbPos / scroll_range;
thumb.style.top = thumbPos < 0 ? 0 : (thumbPos > scroll_range ? scroll_range : thumbPos) + 'px';
content.style.top = thumbPos < 0 ? 0 : (thumbPos > scroll_range ? -diff_height : newScroll) + 'px';
}
function dragThumb(e) {
scrollContent(e.clientY - (thumb_height / 2));
}
function stopDrag(e) {
document.removeEventListener('mousemove', dragThumb);
}
thumb.addEventListener('mousedown', function(e) {
e.preventDefault();
document.addEventListener('mousemove', dragThumb);
});
document.addEventListener('mouseup', stopDrag);
document.addEventListener("mouseleave", stopDrag);
document.addEventListener('wheel', function(e) {
if (!e.ctrlKey) {
scrollContent(parseInt(thumb.style.top) + (e.deltaY < 0 ? -100 : 100));
}
});
window.addEventListener('resize', resetScroll);
//initial reset
document.scrollTop = 0;
thumb.style.top = 0;
resetScroll();
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#container {
position: relative;
overflow: hidden;
}
#content {
position: relative;
top: 0;
right: 0;
width: 100vw;
height: 100vh;
padding-right: 40px;
}
#scrollbar {
position: absolute;
right: 5px;
top: 0;
width: 30px;
height: 100%;
z-index: 10;
}
#track {
position: absolute;
top: 0;
right: 5px;
width: 20px;
height: 100%;
border-right: 3px solid #000;
border-left: 3px solid #000;
}
#thumb {
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 32px;
border: 1px solid #000;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
border-radius: 50%;
background-color: white;
}
.white_mask {
position: absolute;
top: -3px;
right: 6px;
width: 14px;
height: 30px;
background-color: white;
}
.white_mask.big {
top: -4px;
right: 3px;
width: 22px;
height: 36px;
}
#thumb_mask {
position: absolute;
top: -4px;
left: 2px;
width: 24px;
height: 36px;
overflow: hidden;
}
.quarter_border {
position: absolute;
width: 20px;
height: 25px;
border-right: 3px solid #000;
border-left: 3px solid #000;
border-radius: 50%;
background-color: white;
}
.quarter_border.top {
top: -13px;
}
.quarter_border.right {
right: -15px;
}
.quarter_border.bottom {
bottom: -13px;
}
.quarter_border.left {
left: -15px;
}
#red_dot {
position: absolute;
top: 0;
left: 0;
width: 28px;
height: 28px;
border: 2px solid white;
border-radius: 50%;
background-color: #9c0000;
}
<div id="container">
<div id="scrollbar">
<div id="track"></div>
<div id="thumb">
<div class="white_mask"></div>
<div class="white_mask big"></div>
<div id="thumb_mask">
<div class="quarter_border top right"></div>
<div class="quarter_border bottom right"></div>
<div class="quarter_border top left"></div>
<div class="quarter_border bottom left"></div>
</div>
<div id="red_dot"></div>
</div>
</div>
<div id="content">
<p>first</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
<p>last</p>
</div>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With