DEV Community

Leonardo Schmitt
Leonardo Schmitt

Posted on

How to make a scroll indicator bar with JS, HTML and CSS easily and explained 🖱️

Chances are you have already ran into sites where there is a bar, usually located in the header, that increase or decrease according to the page scroll. So, that's what this post is about.

🔍 Overview

Basically, what we want to do is to check where is the scroll compared to page content size, so if the user have scrolled until the middle of the content, the bar is supposed to fulfill 50%, and so on, always tracking and applying the math to do so.

HTML

After fitting the bar in the HTML body, just like the codepen example above, add some content to be able to see some effect when scrolls happen.

<div class='scroll-bar-wrapper'> 
  <div class='scroll-bar'> </div>
</div>
Enter fullscreen mode Exit fullscreen mode

JavaScript

window.addEventListener('scroll',()=> indicateScrollBar())

function indicateScrollBar() {
const distanceFromPageTop = document.body.scrollTop || document.documentElement.scrollTop;

const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;

const scrolled = (distanceFromPageTop / height) * 100;

document.querySelector(".scroll-bar").style.width =  `${scrolled}%`;

} 
Enter fullscreen mode Exit fullscreen mode
  • In .js file, window.addEventListener('scroll',()=> indicateScrollBar()) tells that whenever the user happens to scroll down or up in the window, indicateScrollBar() is called.
  • In this function, we first encounter distanceFromPageTop, a variable that will receive either document.body.scrollTop or document.documentElement.scrollTop. If the first does not exist or the browser doesn't recognize, or it's falsy, the second is reserved to that variable, so explained by the || (Logical OR) in the middle of them. Almost all browsers consider the first one, a property that give us a number meaning how far in pixels we are from the top. document.body.scrollTop differs from document.documentElement.scrollTop just because the latter deals with the whole HTML document, and the first deals with the body itself. In this case, it doesn't affect which is used.

  • Another variable, so, is declared: height. This one will receive the result of document.documentElement.scrollHeight - document.documentElement.clientHeight. But what does exactly this expression mean?

document.documentElement.scrollHeight gives us a number referring to the height of HTML document, the max number that we can get until the scroll stuck in the bottom, the whole content.

document.documentElement.clientHeight gives us a number referring to the height of HTML document that we can see, that's viewable.

  • scrolled is another variable that received the expression (distanceFromPageTop / height) * 100 which will give us the final number.

  • Lastly, we get the bar via DOM, and applies this final number as the width of the bar, not forgetting to add the % signal.

Illustration of the difference between clientHeight and scrollHeight

Alt Text

CSS

.scroll-bar-wrapper {
  width: 100%;
  height:10px;
  position:fixed;
  top:0;
  left:0;
  background:#CCCCCC;
}

.scroll-bar { 
  width:0;
  height: inherit;
  background: #8D7ECA;
}

Enter fullscreen mode Exit fullscreen mode

Now in .css file, we style the bar. The bar wrapper covers 100% of the screen, also fixed at the top, so even in the scrolls it appears. Moreover, the .scroll-bar, the visual bar in itself, receive initially width:0, as it will change with the user scrolls. As well, the same height as the wrapper, its father, and the color to make it all visual.

You can make lots of different styles. This is just a sample with the essence. For instance, you could make the bar as a pseudo-element of main, so avoiding HTML directly, just like so:

✔️ Windup

I wish you found it interesting or learn sth. Goodbye! 👋

Top comments (7)

Collapse
 
robole profile image
Rob OLeary

Hi Leonardo,

I wrote about the same topic a while ago.

I chose to use <progress> rather than <div>. It is better for accessibility, but requires more effort to style.

I discussed optimization using requestAnimation and debouncing also.

I would consider doing both of these if you are putting this into a live website.

All the best

Collapse
 
eecolor profile image
EECOLOR

Nice article. I have one suggestion. Do your DOM write inside a requestAnimationFrame callback.

I took this explanation from some code I wrote a while ago:

/*
  This construct is here to make sure we think about the context when
  writing to the DOM.

  The general rule is:
  - do not read from the DOM inside of an animation frame
  - do not write to the DOM outside of an animation frame

  The reason for this is that reads from the DOM could cause a 
  trigger of expensive layout calculations. This is because the 
  browser will ensure that the values you read are actually correct.
  The expensive calculation will only be triggered if the related values 
  are actually changed since the last paint.

  So to ensure we never have this problem, we ensure the writes 
  (changing values) to be in the part of the frame that is executed
  right before the layout / paint. Reads should never be done 
  from inside of an animation frame; you never know what other
  animation frames have been requested and have performed 
  writes already.

  This shows the effect of calling `requestAnimationFrame` at different positions:
  | non animation frame | animation frame | layout / paint | non animation frame | animation frame |
  |         1->         |    ->1 2->      | -------------- |                     |       ->2       |

  Calling `requestAnimationFrame` from a non animation frame 
  causes the code to be executed in the same frame, but at the
  end of the frame, right before layout / paint.

  Calling `requestAnimationFrame` from an animation frame
  causes the code the be executed at the end of the next frame.
*/
export const writeToDOM = {
  using({ currentlyInAnimationFrame }, f) {
    const context = currentlyInAnimationFrame ? 'fromAnimationFrame' : 'fromNonAnimationFrame'
    writeToDOM[context](f)
  },
  fromAnimationFrame(f) { f() },
  fromNonAnimationFrame(f) { window.requestAnimationFrame(f) }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
korbraan profile image
Cédric Rémond

Good post!

I wondered if this could be done without adding elements to the HTML, here is my slightly modified version: codepen.io/Korbraan/pen/bGgGPbO :)

Collapse
 
leonardoschmittk profile image
Leonardo Schmitt

That's awesome! In fact, not adding elements to the HTML might be better... thanks for pointing out, I'm going to comment it in the post soon :)

Collapse
 
imahmoud profile image
Mahmoud Ibrahiam

Nice article, Thank you

Collapse
 
devggaurav profile image
Gaurav

Amazing article. neatly explained

Collapse
 
huydzzz profile image
Pơ Híp

you are kind