How to dynamically activate menu tabs using Vanilla JavaScript?

I have been exploring this Codepen project and am attempting to replicate it using vanilla JS (as my company does not use jQuery).

My progress so far includes adjusting the line width correctly when clicking on a menu item. However, I am struggling to make it stretch as shown in the Codepen. To keep track of numbers, I added a custom attribute called index and applied a class for easy targeting. If there is a simpler way to achieve this, please feel free to suggest changes to what I have already implemented.

Update: I managed to make it work moving left but not right. Additionally, it only works if the links are positioned next to each other. Any suggestions?

You can view my codepen here: https://codepen.io/ahoward-mm/pen/jOmgxQJ?editors=0010 (optimized for desktop only).

var navList = document.querySelector(".navigation__list");
var navItems = navList.getElementsByClassName("navigation__item");
var navLine = document.querySelector(".navigation__line");

for (var i = 0; i < navItems.length; i++) {
  navItems[i].classList.add(`navigation__item--${i + 1}`);
  navItems[i].setAttribute("index", `${i + 1}`);

  var prevItem = 0;
  var currentItem = 1;

  navItems[i].addEventListener("click", function() {
    var current = document.getElementsByClassName("active");

    if (current.length > 0) {
      current[0].className = current[0].className.replace(" active", "");
    }

    this.className += " active";

    prevItem = currentItem;

    currentItem = this.getAttribute("index");

    navLine.style.width = `${
      document
        .querySelector(`.navigation__item--${currentItem}`)
        .querySelector(".navigation__link")
        .getBoundingClientRect().width +
      document
        .querySelector(`.navigation__item--${prevItem}`)
        .getBoundingClientRect().width
    }px`;

    navLine.style.left = `${
      this.querySelector(".navigation__link").offsetLeft
    }px`;

    setTimeout(function() {
      navLine.style.width = `${
        document
          .querySelector(`.navigation__item--${currentItem}`)
          .querySelector(".navigation__link")
          .getBoundingClientRect().width
      }px`;
    }, 700);
  });
}
body {
  color: #444;
  display: flex;
  min-height: 100vh;
  flex-direction: column;
}

.navigation {
  display: block;
  position: sticky;
  top: -0.5px;
  background-color: #edece8;
  margin: 60px 0 0;
  text-align: center;
}

.navigation__list {
  list-style: none;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0;
  position: relative;
}

.navigation__link {
  color: inherit;
  line-height: inherit;
  word-wrap: break-word;
  text-decoration: none;
  background-color: transparent;
  display: block;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  font-size: 0.875rem;
  font-weight: bold;
  margin: 15px (20px * 2) 0 (20px * 2);
  position: relative;
}

.navigation__line {
  height: 2px;
  position: absolute;
  bottom: 0;
  margin: 10px 0 0 0;
  background: red;
  transition: all 1s;
}

.navigation__item {
  list-style: none;
  display: flex;
}
<nav class="navigation">
  <ul class="navigation__list">
    <li class="navigation__item">
      <a class="navigation__link" href="#">Lorem ipsum</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Dolor</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Consectetur adipiscing</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Donec ut</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Placerat dignissim</a>
    </li>
    <div class="navigation__line"></div>
  </ul>
</nav>

Answer №1

Here's the solution.

I wasn't sure what your JavaScript code was doing, so I took the initiative to rewrite the jQuery code from codepen into vanilla JavaScript completely from scratch. And guess what? It works perfectly!
(It turned out to be a good exercise that took me about 2-2.5 hours.)

Basically, I converted all jQuery functions to their equivalent Vanilla JavaScript counterparts.
For example: $() => document.querySelector(), elem.addClass() => elem.classList.add(), elem.find() => elem.querySelectorAll(), elem.css({prop: val}) => elem.style.prop = val;, and so on.

var nav = document.querySelector(".navigation");
var navLine = nav.querySelector(".navigation__line");

var pos = 0;
var wid = 0;

function setUnderline() {
  var active = nav.querySelectorAll(".active");
  if (active.length) {
    pos = active[0].getBoundingClientRect().left;
    wid = active[0].getBoundingClientRect().width;
    navLine.style.left = active[0].offsetLeft + "px";
    navLine.style.width = wid + "px";
  }
}

setUnderline()

window.onresize = function() {
  setUnderline()
};

nav.querySelectorAll("ul li a").forEach((elem) => {
  elem.onclick = function(e) {
    e.preventDefault();
    if (!this.parentElement.classList.contains("active") &&
      !nav.classList.contains("animate")
    ) {
      nav.classList.add("animate");

      var _this = this;

      nav
        .querySelectorAll("ul li")
        .forEach((e) => e.classList.remove("active"));

      try {

        var position = _this.parentElement.getBoundingClientRect();
        var width = position.width;

        if (position.x >= pos) {
          navLine.style.width = position.x - pos + width + "px";
          setTimeout(() => {
            navLine.style.left = _this.parentElement.offsetLeft + "px";
            navLine.style.width = width + "px";
            navLine.style.transitionDuration = "150ms";
            setTimeout(() => nav.classList.remove("animate"), 150);
            _this.parentElement.classList.add("active");
          }, 300);
        } else {
          navLine.style.width = pos - position.left + wid + "px";
          navLine.style.left = _this.parentElement.offsetLeft + "px";
          setTimeout(() => {
            navLine.style.width = width + "px";
            navLine.style.transitionDuration = "150ms";
            setTimeout(() => nav.classList.remove("animate"), 150);
            _this.parentElement.classList.add("active");
          }, 300);
        }
      } catch (e) {}
      pos = position.left;
      wid = width;
    }
  }
});
body {
  color: #444;
  display: flex;
  min-height: 100vh;
  flex-direction: column;
}

.navigation {
  display: block;
  position: sticky;
  top: -0.5px;
  background-color: #edece8;
  margin: 60px 0 0;
  text-align: center;
}

ul li:not(:last-child) {
  margin-right: 30px;
}

li.active {
  opacity: 1;
}

.navigation__list {
  list-style: none;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0;
  position: relative;
}

.navigation__link {
  color: inherit;
  line-height: inherit;
  word-wrap: break-word;
  text-decoration: none;
  background-color: transparent;
  display: block;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  font-size: 0.875rem;
  font-weight: bold;
  margin: 15px (20px * 2) 0 (20px * 2);
  position: relative;
}

.navigation__line {
  height: 2px;
  position: absolute;
  bottom: 0;
  margin: 10px 0 0 0;
  background: red;
  transition: all 0.3s;
  left: 0;
}

.navigation__item {
  list-style: none;
  display: flex;
}
<link rel="stylesheet" href="style.css">

<body>
  <nav class="navigation">
    <ul class="navigation__list">
      <li class="navigation__item active">
        <a class="navigation__link" href="#">Lorem ipsum</a>
      </li>
      <li class="navigation__item">
        <a class="navigation__link" href="#">Dolor</a>
      </li>
      <li class="navigation__item">
        <a class="navigation__link" href="#">Consectetur adipiscing</a>
      </li>
      <li class="navigation__item">
        <a class="navigation__link" href="#">Donec ut</a>
      </li>
      <li class="navigation__item">
        <a class="navigation__link" href="#">Placerat dignissim</a>
      </li>
      <div class="navigation__line"></div>
    </ul>
  </nav>
  <script src="./script.js"></script>
</body>

Answer №2

So, it seems like your code is functioning well when transitioning to a link that comes before the current one. However, there is an issue when switching to a link that is positioned ahead in the navigation list. In such cases, you need to adjust the `left` property only after adjusting the `width` property.

To enhance clarity and accuracy, I have made several modifications in the code. One major change involved using `classList` for adding/removing classes and utilizing `dataset` for handling custom HTML attributes.

The primary functional amendment relates to checking whether the previous item precedes the current one. If this condition is met, the `left` adjustment should be applied after the `width` adjustment.

Edit: Upon further examination, I identified another issue in the code. Specifically, when moving multiple links forward or backward, the line fails to expand adequately. To address this, I updated the snippet by calculating the distance between the preceding link and the subsequent link, added to the width of the latter link.

var navList = document.querySelector(".navigation__list");
var navItems = navList.querySelectorAll(".navigation__item");
var navLine = document.querySelector(".navigation__line");

var prevItem = 0;
var currentItem = 1;

navItems.forEach((navItem, i) => {
  navItem.classList.add(`navigation__item--${i + 1}`);
  navItem.dataset.index = i + 1;
  navItem.addEventListener("click", function () {
    var current = document.querySelector(".active");

    if (current) {
      current.classList.remove("active");
    }

    this.classList.add("active");

    prevItem = currentItem;

    currentItem = this.dataset.index;
    
    var movingAhead = currentItem > prevItem;
    
    var aheadElem = movingAhead ? document.querySelector(`.navigation__item--${currentItem} .navigation__link`) : document.querySelector(`.navigation__item--${prevItem}  .navigation__link`);
        var behindElem = movingAhead ? document.querySelector(`.navigation__item--${prevItem}  .navigation__link`) : document.querySelector(`.navigation__item--${currentItem} .navigation__link`);
    navLine.style.width = `${(aheadElem.offsetLeft - behindElem.offsetLeft) + aheadElem.getBoundingClientRect().width
    }px`;
    if (!movingAhead) {
      navLine.style.left = `${this.querySelector(".navigation__link").offsetLeft}px`;
    }
    setTimeout(function () {
      var currentLink = document.querySelector(`.navigation__item--${currentItem} .navigation__link`);
      navLine.style.width = `${
        currentLink.getBoundingClientRect().width
      }px`;
      if (movingAhead) {
        navLine.style.left = `${currentLink.offsetLeft}px`;
      }
    }, 700);
  });
})
body {
  color: #444;
  display: flex;
  min-height: 100vh;
  flex-direction: column;
}

.navigation {
  display: block;
  position: sticky;
  top: -0.5px;
  background-color: #edece8;
  margin: 60px 0 0;
  text-align: center;
}

.navigation__list {
  list-style: none;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 0;
  position: relative;
}

.navigation__link {
  color: inherit;
  line-height: inherit;
  word-wrap: break-word;
  text-decoration: none;
  background-color: transparent;
  display: block;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  font-size: 0.875rem;
  font-weight: bold;
  margin: 15px (20px * 2) 0 (20px * 2);
  position: relative;
}

.navigation__line {
  height: 2px;
  position: absolute;
  bottom: 0;
  margin: 10px 0 0 0;
  background: red;
  // width: calc(100% - 80px);
  transition: all 0.8s;
}

.navigation__item {
  list-style: none;
  display: flex;
}
<nav class="navigation">
  <ul class="navigation__list">
    <li class="navigation__item">
      <a class="navigation__link" href="#">Lorem ipsum</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Dolor</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Consectetur adipiscing</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Donec ut</a>
    </li>
    <li class="navigation__item">
      <a class="navigation__link" href="#">Placerat dignissim</a>
    </li>
    <div class="navigation__line"></div>
  </ul>
</nav>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for inserting items into an array of objects?

I have an array of objects with categories and corresponding points, and I need to calculate the total points for each category. { category: A, points:2 }, { category: A points: 3 }, { category: B, points: ...

AWS Lambda Error: Module not found - please check the file path '/var/task/index'

Node.js Alexa Task Problem Presently, I am working on creating a Node.js Alexa Skill using AWS Lambda. One of the functions I am struggling with involves fetching data from the OpenWeather API and storing it in a variable named weather. Below is the relev ...

Guide on setting up a route in Next.js

Recently, I developed a simple feature that enables users to switch between languages on a webpage by adding the language code directly after the URL - i18n-next. Here's a snippet of how it functions: const [languages, ] = React.useState([{ langua ...

What is the procedure for invoking a function when the edit icon is clicked in an Angular application

My current Angular version: Angular CLI: 9.0.0-rc.7 I am currently using ag-grid in my project and I encountered an issue when trying to edit a record. I have a function assigned to the edit icon, but it is giving me an error. Error message: Uncaught Re ...

Display the data submitted from a PHP form on an HTML webpage

In order to create an online quiz utilizing AJAX, I have developed a registration form where users can input their information. The PHP file will validate the details and return a username if they are correct. My goal now is to redirect users directly to ...

What is the method to switch between radio buttons on a webpage?

Here is an example of HTML code: <input type="radio" name="rad" id="Radio0" checked="checked" /> <input type="radio" name="rad" id="Radio1" /> <input type="radio" name="rad" id="Radio2" /> <input type="radio" name="rad" id="Radio4" /& ...

Modify the style of ckeditor's <span> attribute

When using CKEditor in text areas of my form, I encountered an issue with the default style not matching the black background of the site. Here is the basic toolbar setup: CKEDITOR.replace( 'editor1', { toolbar : [ [ 'Bold', &ap ...

A method to verify the presence of a specific element within a list using JavaScript

I'm trying to validate if the list retrieved from the application contains the expected element. Can you please review my code and let me know where I might be making a mistake? this.verifyOptionsInDropdown = async function(){ var optionList = a ...

Adjusting the widths of <input> and <select> elements

I am facing an issue with aligning an input text box under a select element, where the input is intended to add new items to the select. I want both elements to have the same width, but my attempts have not been successful (as I do not wish to specify diff ...

Resizable Bootstrap column-md-4

I need to make a bootstrap column fixed position (col-md-4). This is for a shop layout with 2 columns: Shop content (long & scrollable) col-md-8 Cart content (short & fixed so it stays visible while scrolling the shop content) col-md-4 I'm ...

Sending an object from ng-repeat to a different page for the purpose of showcasing its contents

Currently, I am immersed in a project that involves MySQL, Angular, Express, and Node. Within this project, I have an array of objects displayed using ng-repeat. The goal is to allow users to click on a specific item and navigate to another page where they ...

Ensure that the divs are properly aligned with the paragraphs

I received assistance previously, and I am seeking additional help in maintaining the position or adjusting the size of elements to fit different window viewport sizes such as tablets, mobile devices, or varying monitor sizes while keeping the appearance c ...

Transfer a table row between tables - AngularJS

I am seeking guidance on how to implement ng-repeat in order to transfer data from one table to another. Essentially, I have a "master" table that displays data fetched from an API request. Each row in this table has a button labeled "favorite-this-row". W ...

How to eliminate undefined values from a dropdown selection in AngularJS

Blockquote When choosing a material from the column, the first option is showing as undefined. How can I remove undefined from the drop-down list? What changes need to be made in the HTML/JSON data for this to work properly? Blockquote var app = ang ...

How to Retrieve the Text Content of a Button When Clicked in a Button Group Handler

My goal is to create two button groups. When a button in the second group is clicked, I want to dynamically add a new button to the first group with the same label as the clicked button. Using var name = this.textContent works well when the click handler ...

Modify the structure of the JSON string

My JSON string is structured like this: [ { "queryResult": { "A": "12-04-2014", "B": 1 } }, { "queryResult": { "A": "13-04-2014", "B": 2 } }, { "qu ...

Having trouble with accessing the upvote/downvote input within a Django template

Currently, I'm trying to retrieve upvote/downvote data from a Django template in the following manner: <form method="POST" action="{% url 'vote' %}" class="vote_form"> {% csrf_token %} <input type="hidden" id="id_value" name="valu ...

An error will occur if you try to modify the state of a component from outside the component

Creating a modal component that triggers a bootstrap modal from any section of the application and then defines custom states for that component externally. It is functional, however, an error message consistently appears upon opening the modal, and I am u ...

css - design your layout with floating div elements

I used to design layouts using tables, but now I'm trying it out with divs. However, I'm still struggling to get the layout right. It's not as straightforward as using tables. For instance, my code looks like this: var html_output = "< ...

Combining numerous draggable and droppable functionalities onto a single element

I attempted to include two draggable stop event handlers in a paragraph. However, only the second one seems to fire. How can I ensure that both of them trigger? <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/jq ...