Create website walkthrough assistant in JavaScript

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.

HTML Layout for 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,

  1. That will highlight the element
  2. 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.

Highlighted DOM Element

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;
}

Highlighted element with Popover

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]);