Skip

How To Build a Dropdown Menu with Vanilla JavaScript

So, let's say you're working within a framework, like WordPress, and you don't have great access to the generated code. Here's how you might work around that to implement some nifty dropdown menus.

View source code on GitHub

The HTML

View HTML on GitHub

Here’s the key takeaway(s):

  • Make sure your container element has the data-dropdown=".classname-of-dropdown"
  • Ideally, the element in you define in that attribute will have a sub menu in the form of a <ul>
<header data-dropdown=".menu-item-has-children">
  <ul class="dropdown-menu">
    <li class="menu-item">
      <a href="/">Home</a>
    </li>
    <li class="menu-item menu-item-has-children">
      <a href="/classes">Classes</a>
      <ul class="sub-menu">
        <li class="menu-item">
          <a href="/class-type-i/">Class Type I</a>
        </li>
        <li class="menu-item">
          <a href="/class-type-ii/">Class Type II</a>
        </li>
        <li class="menu-item">
          <a href="/class-type-iii/">Class Type III</a>
        </li>
      </ul>
    </li>
    <li class="menu-item">
      <a href="/pricing/">Pricing</a>
    </li>
    <li class="menu-item">
      <a href="/policies-rules/">Policies and Rules</a>
    </li>
  </ul>
</header>

The CSS

We need to make sure the .menu-item has a relative position and also show the .sub-menu when aria-active is set to true.

.menu-item {
  position: relative;
}

.sub-menu {
  display: none;

  position: absolute;
}

.menu-item-has-children[aria-expanded=true] .sub-menu {
  display: block;
}

The JavaScript

Dropdowns

Full file on GitHub

First we need to check for our data-dropdown element(s) to see if they exist.

If they don’t, we return early and go about the rest of our day.

Otherwise, we get each of our data-dropdown elements and pass them to our Dropdown function:

function Dropdowns() {
  const dropdowns = document.querySelectorAll("[data-dropdown]");
  if (!dropdowns) return;
  dropdowns.forEach(Dropdown);
}

The Dropdown Function

This function gets every HTML element we expect to have a dropdown and passes them to our DropdownElement function where we can finally get to work!

function Dropdown(wrapper) {
  const elSelector = wrapper.dataset.dropdown;
  const elements = wrapper.querySelectorAll(elSelector);
  elements.forEach(DropdownElement);
}

The DropdownElement Function

This function listens for when the parent of the submenu is focused, hovered, touched, or clicked and reacts accordingly.

function DropdownElement(el) {
  const subMenu = el.querySelector("ul");
  if (!subMenu) return;
  const subMenuId = `submenu-${el.id}` || `submenu-${Math.random()}`;
  subMenu.id = subMenuId;
  el.setAttribute("aria-controls", subMenuId);

  let open = false;
  const link = el.querySelector("a");

  const onLinkClick = (e) => e.preventDefault();
  const closeSubMenu = () => updateOpen(false);
  const openSubMenu = () => updateOpen(true);

  function onLinkTouch(e) {
    e.preventDefault();
    updateOpen(!open);
  }

  function onElMouseenter() {
    openSubMenu();
    el.classList.add("has-hover");
  }

  function onElMouseleave() {
    closeSubMenu();
    el.classList.remove("has-hover");
  }

  function updateOpen(state) {
    open = state;
    el.setAttribute("aria-expanded", open);
  }

  el.addEventListener("mouseenter", onElMouseenter);
  el.addEventListener("mouseleave", onElMouseleave);
  link.addEventListener("click", onLinkClick);
  link.addEventListener("touchend", onLinkTouch);

  el.addEventListener("focusin", () => {
    openSubMenu();
    el.classList.add("has-focus");
  });

  el.addEventListener("focusout", (e) => {
    if (el.contains(e.relatedTarget)) return;
    closeSubMenu();
    el.classList.remove("has-focus");
  });
}

Conclusion

That’s all folks! In the next article, we’ll focus on making these dropdown menus animate!