In this tutorial, we will see how to create a walkthrough assistant in JavaScript that walk through the website’s functionality and features.
What are we going to build?
This is similar to the IntroJS library that takes an array of steps with the element and the information to be displayed and walk-through first-time users through the website.
I highly recommend going through the live demo to get a good understanding of how it works and what actually we are going to build.
This question is part of the Frontend System Design Series.
How are we going to build it?
Let us see, how we can create something similar in plain JavaScript. We will try to achieve the functionality and keep the focus on that rather on the user interface.
I have created this dummy HTML layout that represents a simple website with a header, footer, and a main area with simple grids. Each element is represented by a unique id and we are going to use this id to select and navigate to and fro during the walk-through.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Walkthrough</title> <style> .section{ height: 100px; display: flex; align-items: center; justify-content: center; background: yellow; margin-bottom: 5px; font-size: 25px; } #wrapper{ display: flex; align-items: center; justify-content: center; flex-wrap: wrap; } .block{ flex: 0 350px; width: 350px; display: inline-flex; align-items: center; justify-content: center; height: 100px; background: red; margin: 5px; font-size: 40px; color: #000; } </style> </head> <body> <div id="container"> <header id="header" class="section"> Header </header> <div id="wrapper"> <div id="1" class="block">1</div> <div id="2" class="block">2</div> <div id="3" class="block">3</div> <div id="4" class="block">4</div> <div id="5" class="block">5</div> <div id="6" class="block">6</div> <div id="7" class="block">7</div> <div id="8" class="block">8</div> <div id="9" class="block">9</div> <div id="10" class="block">10</div> <div id="11" class="block">11</div> <div id="12" class="block">12</div> <div id="13" class="block">13</div> <div id="14" class="block">14</div> <div id="15" class="block">15</div> <div id="16" class="block">16</div> <div id="17" class="block">17</div> <div id="18" class="block">18</div> </div> <footer id="footer" class="section"> Footer </footer> </div> </body> </html>
For highlighting we are going to create two JavaScript functions,
- That will highlight the element
- Will create a small modal with next and previous buttons for navigation to the next step.
Highlight the selected element
To highlight element, we will get its dimension using the getBoundingClientRect() method and create a new element, and append this element to the body.
As these highlights are overlayed over the DOM elements, these will be absolutely positioned, thus we can append them in the immediate parent or in the body.
const highlightHelper = (elementDimension) => { // calculate the css poisition // where the highlighter will be placed let top = elementDimension.top; let left = elementDimension.left; let width = elementDimension.width; let height = elementDimension.height; // create a new element with an id // and add a style to it const ele = document.createElement('div'); ele.id = "lb-highlight"; ele.style=` position: absolute; top: ${top - 4}px; left: ${left - 4}px; width: ${width}px; height: ${height}px; transition: border .2s ease; `; // append the element to the parent document.getElementById("wrapper").appendChild(ele); // add the border style late to take an effect setTimeout(() => { ele.style.border = "4px solid #000"; }, 0); }
This function accepts the dimension of the element and generates a new element and places it over the selected element, once it is placed the border
style property is added later so that the transition of highlighting can be visible. It also has an ID attached to it, that will use to remove the element, when we navigate to the next step.
I am appending it to the immediate parent of the element.
Add a popover with navigation buttons
Similarly as we highlighted the element, we will also add a popover below the element with the navigation buttons, the position for placement of the popover will be calculated based on the element dimension.
// modal const popover = (elementDimension) => { // calculate the css poisition // where the highlighter will be placed let bottom = elementDimension.bottom; let left = elementDimension.left; let right = elementDimension.right; // create a new element with an id // and add a style to it const ele = document.createElement("div"); ele.id = "lb-popover"; ele.style = ` position: absolute; top: ${bottom + 5}px; left: ${((left + right) / 2) - 50}px; background: #fff; width: 100px; height: 100px; `; // add the navigation button ele.appendChild(navigationButton()); // apend to the parent of the element document.getElementById("wrapper").appendChild(ele); } // navigation button const navigationButton = () => { // create the next button const nextButton = document.createElement('button'); nextButton.textContent = "next"; // create the prev button const prevButton = document.createElement('button'); prevButton.textContent = "prev"; // create a fragment and these two buttons to it const fragment = document.createDocumentFragment(); fragment.appendChild(prevButton); fragment.appendChild(nextButton); return fragment; }
Navigating through steps
Now once the navigation buttons are ready if they are clicked we have to move forward or backward to the next step, to make it functional, let’s create an array of steps that will hold the ID of the DOM elements that have to be highlighted and an index variable to track steps.
const steps = ["3", "header", "8", "12", "footer", "5"]; let index = 0;
Attach the event listener to the buttons, and when they are clicked, invoke a function that will remove the existing highlighting and move to the next step. For this, we will create a function, that will help us in abstract things.
Adding click event listener to the Navigation buttons
const navigationButton = () => { // create the next button with click event listener const nextButton = document.createElement('button'); nextButton.textContent = "next"; nextButton.addEventListener('click', function(){ // move the next step if(index < steps.length - 1){ highlight(steps[++index]); } }); // create the previous button with click event listener const prevButton = document.createElement('button'); prevButton.textContent = "prev"; prevButton.addEventListener('click', function(){ // move the prev step if(index > 0){ highlight(steps[--index]); } }); // create a fragment and these two buttons to it const fragment = document.createDocumentFragment(); fragment.appendChild(prevButton); fragment.appendChild(nextButton); return fragment; }
Helper function for navigation
// helper function const highlight = (id) => { // remove the existing highlighted elements document.getElementById("lb-highlight")?.remove(); document.getElementById("lb-popover")?.remove(); // get the element with the ID const element = document.getElementById(id); // get the element dimension const elementDimension = element.getBoundingClientRect(); // highlight the element highlightHelper(elementDimension); // add the popover with navigation button popover(elementDimension); }
The final part pending is scrolling to the element, that is not in the viewport, suppose in the next step we have to move to the footer from the header and the footer is not in the viewport, thus we will have to scroll to the footer first.
For this, we are going to use the window.scrollTo() method.
// helper function to scroll to element smoothly const scrollTo = (element) => { const eleTop = element.offsetTop; window.scrollTo({top: eleTop, behavior: "smooth"}); }
Add this in the helper function, before highlighters are generated.
// helper function const highlight = (id) => { ... // get the element with the ID const element = document.getElementById(id); // scroll to the element scrollTo(element); // get the element dimension const elementDimension = element.getBoundingClientRect(); ... }
And also updating the highlighter and popover CSS position as getBoundingClientRect() gives the dimension respect to the viewport thus as we are scrolling, the value changes, we need to consider the scrollY and scrollX position to correctly calculate the element absolute positioning.
So in the top, bottom, right, and left values add the window.scrollY and window.scrollX values.
const highlightHelper = (elementDimension) => { let top = elementDimension.top + window.scrollY; let left = elementDimension.left + window.scrollX; .... } const popover = (elementDimension) => { let bottom = elementDimension.bottom + window.scrollY; let left = elementDimension.left + window.scrollX; ... }
Putting everything together
const steps = ["3", "header", "8", "12", "footer", "5"]; let index = 0; // helper function const highlight = (id) => { // remove the existing highlighted elements document.getElementById("lb-highlight")?.remove(); document.getElementById("lb-popover")?.remove(); // get the element with the ID const element = document.getElementById(id); // get the element dimension const elementDimension = element.getBoundingClientRect(); // highlight the element highlightHelper(elementDimension); // add the popover with navigation button popover(elementDimension); } const highlightHelper = (elementDimension) => { // calculate the css poisition // where the highlighter will be placed let top = elementDimension.top + window.scrollY; let left = elementDimension.left + window.scrollX; let width = elementDimension.width; let height = elementDimension.height; // create a new element with an id // and add a style to it const ele = document.createElement('div'); ele.id = "lb-highlight"; ele.style=` position: absolute; top: ${top - 4}px; left: ${left - 4}px; width: ${width}px; height: ${height}px; transition: border .2s ease; `; // append the element to the parent document.getElementById("wrapper").appendChild(ele); // add the border style late to take an effect setTimeout(() => { ele.style.border = "4px solid #000"; }, 0); } const popover = (elementDimension) => { // calculate the css poisition // where the highlighter will be placed let bottom = elementDimension.bottom + window.scrollY; let left = elementDimension.left + window.scrollX; let right = elementDimension.right; // create a new element with an id // and add a style to it const ele = document.createElement("div"); ele.id = "lb-popover"; ele.style = ` position: absolute; top: ${bottom + 5}px; left: ${((left + right) / 2) - 50}px; background: #fff; width: 100px; height: 100px; `; // add the navigation button ele.appendChild(navigationButton()); // apend to the parent of the element document.getElementById("wrapper").appendChild(ele); } const navigationButton = () => { // create the next button with click event listener const nextButton = document.createElement('button'); nextButton.textContent = "next"; nextButton.addEventListener('click', function(){ // move the next step if(index < steps.length - 1){ highlight(steps[++index]); } }); // create the previous button with click event listener const prevButton = document.createElement('button'); prevButton.textContent = "prev"; prevButton.addEventListener('click', function(){ // move the prev step if(index > 0){ highlight(steps[--index]); } }); // create a fragment and these two buttons to it const fragment = document.createDocumentFragment(); fragment.appendChild(prevButton); fragment.appendChild(nextButton); return fragment; } // helper function to scroll to element smoothly const scrollTo = (element) => { const eleTop = element.offsetTop; window.scrollTo({top: eleTop, behavior: "smooth"}); } // initiate first step highlight(steps[index]);