DOM Traversal: closest() in Vanilla JavaScript and jQuery

Abstract image used to represent DOM Traversal: closest() in Vanilla JS & jQuery
Image by Jackson So.

Traversing the DOM is an essential part of developing interactivity into a website, making it feel more alive. For example, developing the classic dropdown navigation (and corresponding mobile version). Clicking on a dropdown link or a button to expand a submenu is a pretty common interaction on the web, and every time I've built a mobile menu, it has required some form of DOM traversal and manipulation.

In this post, we're going to take a look at the .closest() method in jQuery, and then delve into some of the ways we can achieve the same functionality with straight, vanilla JavaScript. Thankfully, you'll find it's very straightforward.


.closest() in jQuery

So, first things first, what does .closest() actually do? Here is how the jQuery docs describe it:

For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.

Essentially, it programmatically works its way up through the DOM tree, starting at a specific element (which you've defined) and testing each parent against the selector you're searching for.

This means that you can use interactions on a child element to affect the parent, grandparent, or any other ancestor element, which is incredibly handy.

Let's take a look at it in the context of a mobile menu:

<header>
  <nav>
    <ul>
      <li>
        <button type="button" class="trigger" aria-expanded="false"
          aria-controls="about-submenu">About Us</button>
        <ul id="about-submenu" hidden>
          <li><a href="/contact">Contact Us</a></li>
          <li><a href="/our-work">Our Work</a></li>
          <li><a href="/testimonials">Testimonials</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>

<script>
  $('.trigger').click(function () {
    var expanded = $(this).attr('aria-expanded') !== 'true';
    $(this).attr('aria-expanded', String(expanded));
    $('#' + $(this).attr('aria-controls')).prop('hidden', !expanded);
    $(this).closest('nav').toggleClass('mobile-submenu-active', expanded);
  });
</script>

This is a small disclosure example: a native button works with the keyboard, aria-controls identifies its submenu, and aria-expanded follows the open state. The submenu starts hidden. Clicking the button toggles both its visibility and the mobile-submenu-active class on the ancestor nav, found through .closest(). There is no animation here, which keeps the behaviour easy to compare with the native example below.

So, it is demonstrably fairly easy to handle this using jQuery but as I've discussed a few times before jQuery is a luxury, and oftentimes we don't need to weigh down our projects by including a complete additional library for something that can also be accomplished straightforwardly using pure JavaScript.

January 2015: Firefox 35 added Element.closest(), with other major browsers following later. The native section below therefore postdates this article's original July 2014 publication; the jQuery and manualparent approaches reflect the earlier browser context.


.closest() in pure, vanilla JavaScript

Why use a framework when we already have access to the tools to do the job without it? Usually, the benefit of jQuery is its ability to offer a standardised way where browser implementations and support are fragmented. This is very much the case with JavaScript's very own closest() method.

This works in essentially the same way as the jQuery implementation does. According to the MDN Web Docs:

The closest() method traverses the Element and its parents (heading towards the document root) until it finds a node that matches the provided selector string. Will return itself or the matching ancestor. If no such element exists, it returns null.

And following the same example as we used above, in action it looks a little like this:

<header>
  <nav>
    <ul>
      <li>
        <button type="button" onclick="mobileSubmenuActive(this);"
          aria-expanded="false" aria-controls="about-submenu">About Us</button>
        <ul id="about-submenu" hidden>
          <li><a href="/contact">Contact Us</a></li>
          <li><a href="/our-work">Our Work</a></li>
          <li><a href="/testimonials">Testimonials</a></li>
        </ul>
      </li>
    </ul>
  </nav>
</header>

<script>
  function mobileSubmenuActive(button) {
    var expanded = button.getAttribute('aria-expanded') !== 'true';
    var submenu = document.getElementById(button.getAttribute('aria-controls'));
    var nav = button.closest('nav');
    button.setAttribute('aria-expanded', String(expanded));
    submenu.hidden = !expanded;
    if (expanded) {
      nav.classList.add('mobile-submenu-active');
    } else {
      nav.classList.remove('mobile-submenu-active');
    }
  }
</script>

The native example opens and closes the same submenu, updates aria-expanded, and toggles the ancestor class, just like the jQuery version. The remaining distinction is browser support: Internet Explorer does not provide native Element.closest(), so that lookup needs a polyfill there.

Thankfully, MDN also offers a polyfill that will allow support for IE9+:

if (!Element.prototype.matches) {
  Element.prototype.matches =
    Element.prototype.msMatchesSelector ||
    Element.prototype.webkitMatchesSelector;
}

if (!Element.prototype.closest) {
  Element.prototype.closest = function (s) {
    var el = this;
    do {
      if (Element.prototype.matches.call(el, s)) return el;
      el = el.parentElement || el.parentNode;
    } while (el !== null && el.nodeType === 1);
    return null;
  };
}

As you might imagine, this won't be quite as performant as in more modern browsers but it will help futureproof your code a little. MDN does also present a further workaround for even earlier versions of IE but warns that it could cause huge lag spikes and that it only supports CSS2 selectors so perhaps best avoided.

All in all, I'm a big advocate of removing jQuery wherever possible. As the JavaScript specification continues to mature that becomes easier to achieve. For now, though, where we are caught supporting outdated browsers, it is much harder to achieve without adding more weight to the project's client side.

Postscript

June 2026: The DOM traversal idea here still matters, especially for delegated events and menus, but the jQuery comparison belongs to an older implementation context. The native Element.closest() example reflects later browser support: it was not broadly available when this article was first published in July 2014. For new work I would usually handle this inside the component or interaction model rather than adding jQuery to a modern stack.

Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.