Progress Steps Bar
Code Breakdown
Introduction
A "Progress Step Bar" can be useful for many applications that need to show a progress for something. Amazon for example utilizes one in it's tracking page where it shows the progress of your shipment. This could be useful in many applications and we're going to see how to build one here.
The complete source code for this project can be downloaded here.
the HTML
The HTML is very straight forward here. We'll set up a main container <div>. Inside, we'll need a <div> for a line we want to progress along, and another <div> for each step along the way.
The number of steps is determined by us when we build the <div>'s. For example, if we want four steps, we need four <div>'s. If we want five steps, we need five <div>'s, and so on.Here is the code:
- HTML
- <div class="container">
- <div class="progress-container">
- <div id="progress" class="progress"></div>
- <div class="circle active">1</div>
- <div class="circle">2</div>
- <div class="circle">3</div>
- <div class="circle">4</div>
- </div>
- <button id="prev" class="btn" disabled>Prev</button>
- <button id="next" class="btn">Next</button>
- <div class="progress-container">
- </div>
- the progress-container <div> sets up the container that holds the progress bar, and progress number circles.
- the progress <div> is for the progress bar.
- the circle <div>'s are used to display the number circles, with "1" being the active circle, initially.
We'll control the length of the progress bar and which circle is active, with JavaScript.
- the id's are used for functionality within the JavaScript.
- the circle class's are used both for styling, and functionality within the JavaScript.
Notice that initially the Prev button is disabled. This functionality is controlled within the JavaScript such that the buttons are only functional when needed. In other words, if the progress bar is at 1, we don't need the Prev button, and when it is at 4, we won't need the Next button. All other progress steps, both buttons will be active.
- when disabled, the buttons will be "greyed out" and non-functional.
the CSS
The CSS isn't too bad, but we've learned a few very interesting things here. For example, we can utilize variables within our CSS file. The way it was described was that, "in this application, we want to assign the variables at the root level".
So the code looks like this:
- :root {
- --line-border-full: #1c7422;
- --line-border-empty: #d1d1d1;
- }
These "variable" definitions must go at the top os the CSS file before they are utilized by the CSS Code. What we're doing here is:
- - defining two variables at the :root level
- - the 1st variable --line-border-full, has a value of #1c7422;
- - the 2nd variable --line-border-empty, has a value of #d1d1d1;
We'll use these variables throughout the CSS file to style the progress bar, circles, and buttons. To utilize the variable, we use the following syntax:
- background-color: var(--line-border-full);
- or
- background-color: var(--line-border-empty);
This example utilizes the variables to set the "background color", but they can be utilized for any CSS property that you want to set the color for.
Also, the variables are not limited to colors. You can set any CSS property and give it an appropriate name to identify the variable.
And the rest of the CSS:
We're setting a flex column layout with main and cross axis's centered, setting a 100vh height, 0 margin and hide any overflow.
- body {
- height: 100vh;
- margin: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: flex-start;
- }
Our main container just sets up some basic alignment.
- container {
- margin-top: 100px;
- text-align: center;
- }
This next bit of styling sets up our progress-container which, if we recall, is the container tha holds our progress bar and circles. We're positioning this <div> as relative to control the alignment of the bar and circles, when we style them below.
We're setting this as a flex row layout which allows us to space the progress circles evenly.
- progress-container {
- display: flex;
- justify-content: space-between;
- position: relative;
- margin-bottom: 30px;
- max-width: 100%;
- width: 350px;
- }
Next we need to define our progress bar. This is a horizontal bar that will grow or shrink when we press Prev and Next buttons respectively.
- progress {
- background-color: var(--line-border-full);
- position: absolute;
- top: 50%;
- left: 0;
- width: 0%;
- height: 4px;
- transform: translateY(-50%);
- z-index: -1;
- transition: 0.7s ease;
- }
Remember, this is just styling the horizontal bar.
- - we're giving it a background color using the variable var(--line-border-full)
- - we positioned the "parent" (progress-container) relative, so we could position this bar absolute within the parent
- - we positioned the bar left: 0 and top: 50% which places it vertically in the middle of the parent, however, it was not centered vertically behind the circle numbers, so
- - we added a transform: translateY(-50%) which moves the bar up 50% of it's height, which centers it behind the circle numbers
- - we gave it a z-index: -1 so the bar would be behind the circles
- - and finally we gave it a transition which will be a slight animation effect when the bar grows or shrinks
Next we want to add a grey line behind our progress bar that just shows as kind of a placeholder for where thr progress bar grows and shrinks. This line will always be there irregardless of the progress bar position.
To accomplish this, we're going to actually insert this grey line using the ::before pseudo-class.
The ::before pseudo-element inserts generated content as the first child of a selected element, appearing before the element's actual content.
Now the progress-container is the "parent" container and we want to insert our grey bar as the first child. Here is the code:
- progress-container::before {
- content: '';
- background-color: var(--line-border-empty);
- position: absolute;
- top: 50%;
- left: 0;
- width: 100%;
- height: 4px;
- transform: translateY(-50%);
- z-index: -1;
- }
So this is essentially the same code as we had for the progress bar we already defined with a few changes.
- - first, when using the ::before pseudo-element you must provide some content, and this content must be the first "styling" call. So with the very first line, we a providing an empty string as our content.
- - next we are using a grey color for the bar so our background-color variable changes
- - this bar will always be visible, will not grow and shrink like our main progress bar, so we set the width to 100%
- - and finally, since this bar is always going to be 100%, we do not need the transition effect, so we removed that line
*note: the instructor chose to use this ::before pseudo class to "insert" the grey background bar underneath our progress bar. We could have just as easily added this bar as the first <div> in our progress-container, with it's own class to style. But since the instructor did it this way, we left it in the code for future reference on how to utilize the ::before pseudo class.
Next we need to style our circles. Here is their code:
- .circle {
- background-color: #f6f7fb;
- color: #5f5f5f;
- font-size: 15px;
- border-radius: 50%;
- width: 36px;
- height: 36px;
- display: flex;
- justify-content: center;
- align-items: center;
- background-color: var(--line-border-empty);
- transition: 0.4s ease;
- }
Pretty straight forward here. Notice that:
- - we are setting the background color with a variable
- - we are also setting a transition that comes in to play when a circle becomes active and changes color. We don't want the color change to be instant, we want it to ease in.
So we need to code for this color change on the circles when they become active:
- .circle.active {
- background-color: var(--line-border-full);
- color: #eeeeee;
- background-color: var(--line-border-empty);
- }
Notice how this defines a style for the circles when they are active. We'll use JavaScript to determine whether the class is active or inactive.
And finally, we need to define the styles for Prev and Next buttons. First, the basic button styling
- // Basic Button
- .btn {
- background-color: var(--line-border-full);
- color: #fff;
- border: 0;
- border-radius: 6px;
- cursor: pointer;
- font-family: inherit;
- padding: 8px 30px;
- margin: 5px;
- font-size: 14px;
- box-shadow: 2px 2px 4px #6b6b6b;
- }
and when you click on a button, remove the shadow, make the text slightly smaller, and move it up slightly. This all gives an animation effect of actually pressing the button.
- // Active Button
- .btn:active {
- transform: scale(0.97);
- font-size: 11px;
- box-shadow: none;
- }
and finally, when the progress bar is at the minimum or maximum positions, we don't want the corresponding button to be "clickable", so we disable it. We're setting the styling here, but the functionality is controlled through JavaScript.
- // Disabled Button
- .btn:disabled {
- background-color: var(--line-border-empty);
- cursor: not-allowed;
- box-shadow: none;
- }
And that will do it for the CSS. Next up, we'll set the functionality with the JavaScript.
the JavaScript
The first thing we need to do is to import our id's and classes for our progress bar, buttons, and circles.
- const progress = document.querySelector('#progress')
- const prev = document.querySelector('#prev')
- const next = document.querySelector('#next')
- const circles = document.querySelectorAll('.circle')
- let currActive = 1;
We are using querySelectorAll to import our <div>'s with a class of circle. This gives us a Node List that behaves kind of like and array in the sense that we can identify each circle <div> by it's idx (index) number. We have four circle <div>'s so our Node List would have a length of 5, and we would reference each individual node item by it's idx (0, 1, 2, 3, 4).
The currActive variable is used to determine which circle is currently active, with circle #1 being set to active initially.
Next we're going to set up our "click events" for the Prev and Next buttons:
- next.addEventListener('click', () => {
- currActive++
- if (currActive > circles.length) {
- currActive = circles.length;
- }
- update();
- })
We're going to increment our active circle by 1. Remember that our length of our Node List for the circle <div>'s is 5, so if the currActive increases beyond 5, set it back to 5. This ensures we never increment past our max number of circles.
We'll do the same for the Prev button, but in reverse:
- prev.addEventListener('click', () => {
- currActive-- <>if (currActive < 1) {
- currActive = 1;
- }
- update();
So we're going to subtract 1 from the current circle index, but if it drops below 1, we set it to 1, ensuring we never go below the first circle <div>.
Notice that both of our "click events" call an update() function that is going to add or remove the active class to our circle <div>'s. Remember that in our style sheet we set the active circles to be formatted differently than the inactive circles.
- function update() => {
- // set active circles for styling
- circles.forEach ((circle, idx) => {
- if (idx < currActive) {
- circle.classList.add('active')
- } else {
- circle.classList.remove('active');
- }
- if (idx < currActive) {
- })
- // set the width of the progress bar
- const actives = document.querySelectorAll('.active');
- progress.style.width = (actives.length - 1) / (circles.length - 1) * 100 + '%';
- // set Prev/Next buttons to active or inactive
- if (currActive === 1) {
- prev.disabled = true;
- } else if (currActive === circles.length) {
- next.disabled = true;
- } else {
- prev.disabled = false;
- next.disabled = false;
- }
- }
We're passing in two variables to the function, our circles Node List, and the idx (index) numbers. This gives us the ability to loop through the node list and compare the index numbers to the current active circle.
We then set all of the circles below the current circle to active, and set all of the other circles to inactive. This sets our styles that we had set up in the styles sheet.
Next we calculate the width of the progress bar based on the number of active circles.
And finally we determine whether the Prev and/or Next buttons are active or inactive. This determination is based on how many current active circles we have.