369

How can I find an element's ancestor that is closest up the tree that has a particular class, in pure JavaScript? For example, in a tree like so:

<div class="far ancestor">
    <div class="near ancestor">
        <p>Where am I?</p>
    </div>
</div>

Then I want div.near.ancestor if I try this on the p and search for ancestor.

rvighne
  • 20,755
  • 11
  • 51
  • 73
  • 2
    please note that the outer div is not a parent, it's an *ancestor* to the `p` element. If you actually only want to get the parent node, you can do `ele.parentNode`. – Felix Kling Mar 01 '14 at 20:10
  • 1
    @FelixKling: Didn't know that terminology; I will change it. – rvighne Mar 01 '14 at 20:10
  • 3
    It's actually the same as we humans use :) The father (parent) of your father (parent) is not your father (parent), it's your grandfather (grandparent), or more generally speaking, your ancestor. – Felix Kling Mar 01 '14 at 20:13

6 Answers6

591

Update: Now supported in most major browsers

document.querySelector("p").closest(".near.ancestor")

Note that this can match selectors, not just classes

https://developer.mozilla.org/en-US/docs/Web/API/Element.closest


For legacy browsers that do not support closest() but have matches() one can build selector-matching similar to @rvighne's class matching:

function findAncestor (el, sel) {
    while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
    return el;
}
the8472
  • 40,999
  • 5
  • 70
  • 122
  • 2
    Still not supported in the current versions of Internet Explorer, Edge and Opera Mini. – kleinfreund Nov 12 '15 at 16:55
  • 2
    @kleinfreund - still not supported in IE, Edge, or Opera mini. http://caniuse.com/#search=closest – evolutionxbox May 04 '16 at 09:01
  • `document` has no `matches` (nor `matchesSelector`) so this polyfill is incorrect. Also, what's with code golfing? – Konrad Dzwinel May 12 '16 at 15:13
  • Also, `(el.matches || el.matchesSelector)(sel)` throws `Illegal invocation` in Chrome. – Konrad Dzwinel May 12 '16 at 15:23
  • @KonradDzwinel, that's odd because `parentElement` should only return objects implementing the Element interface and `matches` is defined on the Element interface. So according to specs the there should never be a case where the method is not available on el when it's invoked. But the second issue you raise is valid. – the8472 May 12 '16 at 17:30
  • 9
    June 2016: Looks like all browsers still havn't caught up – Kunal Jun 23 '16 at 21:39
  • Use polyfill from [here](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) to get `matches` support – Balaji Sivanath Nov 11 '16 at 06:36
  • 3
    There is a [DOM Level 4](https://github.com/WebReflection/dom4) polyfill with `closest` plus many more advanced DOM traversal features. You can pull that implementation on its own or include the whole bundle to get a lot of new features in your code. – Garbee Dec 08 '16 at 13:19
  • 3
    Edge 15 has support, that should cover all major browsers. Unless you count IE11, but that doesn't receive any updates – the8472 May 08 '17 at 19:15
  • polyfill https://cdn.polyfill.io/v2/polyfill.min.js from https://polyfill.io/v2/docs/, this polyfill can cover all major browsers. The size of polyfill is 0 bytes if called from Chrome browser. – allenhwkim Nov 19 '17 at 02:32
  • https://developer.mozilla.org/en-US/docs/Web/API/Element/closest has a polyfill as well. – Thierry Prost Jun 18 '19 at 05:57
209

This does the trick:

function findAncestor (el, cls) {
    while ((el = el.parentElement) && !el.classList.contains(cls));
    return el;
}

The while loop waits until el has the desired class, and it sets el to el's parent every iteration so in the end, you have the ancestor with that class or null.

Here's a fiddle, if anyone wants to improve it. It won't work on old browsers (i.e. IE); see this compatibility table for classList. parentElement is used here because parentNode would involve more work to make sure that the node is an element.

rvighne
  • 20,755
  • 11
  • 51
  • 73
  • 1
    For an alternative to `.classList`, see http://stackoverflow.com/q/5898656/218196. – Felix Kling Mar 01 '14 at 20:08
  • 1
    I fixed the code, but it would still throw an error if there is no ancestor with such a class name. – Felix Kling Mar 01 '14 at 20:11
  • @FelixKling: Could you please explain why `parentElement` was wrong? – rvighne Mar 01 '14 at 20:12
  • 2
    Uh, I thought it didn't exist, [but I was wrong](https://developer.mozilla.org/en-US/docs/Web/API/Node.parentElement). Anyways, `parentElement` is a rather new property (DOM level 4) and `parentNode` exists since... forever. So `parentNode` also works in older browsers, whereas `parentElement` might not. Of course you could make the same argument for/against `classList`, but it doesn't have a simple alternative, like `parentElement` has. I actually wonder why `parentElement` exists at all, it doesn't seem to add any value over `parentNode`. – Felix Kling Mar 01 '14 at 20:16
  • @FelixKling: For example, parentElement will return `null` for parent of the root element, but parentNode will return the `document`. – rvighne Mar 01 '14 at 20:28
  • 1
    @FelixKling: Just edited, it now works fine for the case when no such ancestor exists. I must have gotten overeager with the short original version. – rvighne Mar 01 '14 at 20:30
  • [className](https://developer.mozilla.org/en-US/docs/Web/API/Element.className) is an alternate for classList. – alexK85 Mar 01 '14 at 20:30
  • If you wanted to do it in one expression, you could do `while ((el = el.parentNode) && !el.classList.contains(cls));`. But at some point it becomes less readable. – Felix Kling Mar 01 '14 at 20:32
  • @FelixKling: That won't work because eventually you might get to `document`, and the document object has no `classList`. Perhaps I should revert it to use `parentElement` again? Browser support isn't too bad. – rvighne Mar 01 '14 at 20:34
  • Ah right... damn it. Yeah, use `parentElement` in that case, sorry for messing around with your answer. Just make sure to include a link to the MDN documentation, because I'd assume that people rather know `parentNode` than `parentElement`. – Felix Kling Mar 01 '14 at 20:35
  • 3
    If you want to check for the class name in the parameter element itself, before searching its ancestors, you can simply switch the order of the conditions in the loop: `while (!el.classList.contains(cls) && (el = el.parentElement));` – Nicomak Jan 19 '17 at 06:02
  • @Nicomak - Thanks for this heads up. And to be a little more specific, it would be more accurate to say "in addition to searching its ancestors" instead of "before searching its ancestors". The code as-is will *never* consider the classes of the original `el` element. – rinogo Sep 01 '17 at 15:10
75

Use element.closest()

https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

See this example DOM:

<article>
  <div id="div-01">Here is div-01
    <div id="div-02">Here is div-02
      <div id="div-03">Here is div-03</div>
    </div>
  </div>
</article>

This is how you would use element.closest:

var el = document.getElementById('div-03');

var r1 = el.closest("#div-02");  
// returns the element with the id=div-02

var r2 = el.closest("div div");  
// returns the closest ancestor which is a div in div, here is div-03 itself

var r3 = el.closest("article > div");  
// returns the closest ancestor which is a div and has a parent article, here is div-01

var r4 = el.closest(":not(div)");
// returns the closest ancestor which is not a div, here is the outmost article
simbro
  • 3,372
  • 7
  • 34
  • 46
17

Based on the the8472 answer and https://developer.mozilla.org/en-US/docs/Web/API/Element/matches here is cross-platform 2017 solution:

if (!Element.prototype.matches) {
    Element.prototype.matches =
        Element.prototype.matchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector ||
        Element.prototype.oMatchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        function(s) {
            var matches = (this.document || this.ownerDocument).querySelectorAll(s),
                i = matches.length;
            while (--i >= 0 && matches.item(i) !== this) {}
            return i > -1;
        };
}

function findAncestor(el, sel) {
    if (typeof el.closest === 'function') {
        return el.closest(sel) || null;
    }
    while (el) {
        if (el.matches(sel)) {
            return el;
        }
        el = el.parentElement;
    }
    return null;
}
Shai Coleman
  • 868
  • 15
  • 17
marverix
  • 7,184
  • 6
  • 38
  • 50
15

@rvighne solution works well, but as identified in the comments ParentElement and ClassList both have compatibility issues. To make it more compatible, I have used:

function findAncestor (el, cls) {
    while ((el = el.parentNode) && el.className.indexOf(cls) < 0);
    return el;
}
  • parentNode property instead of the parentElement property
  • indexOf method on the className property instead of the contains method on the classList property.

Of course, indexOf is simply looking for the presence of that string, it does not care if it is the whole string or not. So if you had another element with class 'ancestor-type' it would still return as having found 'ancestor', if this is a problem for you, perhaps you can use regexp to find an exact match.

Josh
  • 376
  • 2
  • 13
6

This solution should work for IE9 and up.

It's like jQuery's parents() method when you need to get a parent container which might be up a few levels from the given element, like finding the containing <form> of a clicked <button>. Looks through the parents until the matching selector is found, or until it reaches the <body>. Returns either the matching element or the <body>.

function parents(el, selector){
    var parent_container = el;
    do {
        parent_container = parent_container.parentNode;
    }
    while( !parent_container.matches(selector) && parent_container !== document.body );

    return parent_container;
}
James
  • 389
  • 4
  • 11