202

In straight up javascript (i.e., no extensions such as jQuery, etc.), is there a way to determine a child node's index inside of its parent node without iterating over and comparing all children nodes?

E.g.,

var child = document.getElementById('my_element');
var parent = child.parentNode;
var childNodes = parent.childNodes;
var count = childNodes.length;
var child_index;
for (var i = 0; i < count; ++i) {
  if (child === childNodes[i]) {
    child_index = i;
    break;
  }
}

Is there a better way to determine the child's index?

Uyghur Lives Matter
  • 18,820
  • 42
  • 108
  • 144
  • 2
    Sorry, am I a complete dolt? There are lots of seemingly learned answers here, but to get all the children nodes don't you need to do `parent.childNodes`, rather than `parent.children`?. The latter only lists the `Elements`, excluding in particular `Text` nodes... Some of the answers here, e.g. using `previousSibling`, are based on using all child nodes, whereas others are only bothered with children which are `Element`s... (!) – mike rodent Oct 12 '17 at 11:17
  • @mikerodent I don't remember what my purpose was when I initially asked this question, but that's a key detail that I wasn't aware of. Unless you're careful, `.childNodes` should definitely be used instead of `.children`. The top 2 posted answers will give different results as you pointed out. – Uyghur Lives Matter Oct 12 '17 at 13:49
  • When planning on doing thousands of lookups over 1000+ nodes, then attach information to the node (e.g. via child.dataset). The goal would be to convert a O(n) or O(n^2) algorithm into a O(1) algorithm. The downside is that if nodes are added and removed regularly, the associated position information attached to the nodes will have to be updated too, which might not result in any performance gains. The occasional iteration isn't a big deal (e.g. click handler) but repeated iteration is problematic (e.g. mousemove). – CubicleSoft May 03 '20 at 13:09

14 Answers14

196

I've become fond of using indexOf for this. Because indexOf is on Array.prototype and parent.children is a NodeList, you have to use call(); It's kind of ugly but it's a one liner and uses functions that any javascript dev should be familiar with anyhow.

var child = document.getElementById('my_element');
var parent = child.parentNode;
// The equivalent of parent.children.indexOf(child)
var index = Array.prototype.indexOf.call(parent.children, child);
KhalilRavanna
  • 5,768
  • 3
  • 28
  • 24
  • 9
    var index = [].indexOf.call(child.parentNode.children, child); – cuixiping Aug 15 '14 at 17:04
  • 37
    Fwiw, using `[]` creates an Array instance every time you run that code, which is less efficient for memory and GC vs using `Array.prototype`. – Scott Miles Aug 29 '14 at 00:50
  • @ScottMiles May i ask to explain what you've said a little more? Doesn't `[]` get clean on memory as a garbage value? – mrReiha Jun 24 '15 at 07:43
  • 20
    To evaluate `[].indexOf` the engine has to create an array instance just to access the `indexOf` implementation on the prototype. The instance itself goes unused (it does GC, it's not a leak, it's just wasting cycles). `Array.prototype.indexOf` accesses that implementation directly without allocating an anonymous instance. The difference is going to be negligible in almost all circumstances, so frankly it may not be worth caring about. – Scott Miles Jun 24 '15 at 17:03
  • Which is faster, this answer or @Liv's answer? – Jessica Feb 14 '16 at 20:41
  • 3
    Beware of error in IE! Internet Explorer 6, 7 and 8 supported it, but erroneously includes Comment nodes. Source" https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children#Browser_compatibility – Luckylooke May 04 '17 at 06:53
  • Any reason for `.children` vice `.childNodes`? – Evan Hendler Jun 22 '17 at 16:02
  • You'll filter out all the non-element Nodes, but you lose cross browser support; that and the code could be used to find unencapsulated strings with `.childNodes`. – Evan Hendler Jun 22 '17 at 16:05
  • 1
    in modern javascript you can use `[ ...parent.children ].indexOf ( child )` – Nolo Jul 13 '21 at 04:30
191

ES6:

Array.from(element.parentNode.children).indexOf(element)

Explanation :

  • element.parentNode.children → Returns the brothers of element, including that element.

  • Array.from → Casts the constructor of children to an Array object

  • indexOf → You can apply indexOf because you now have an Array object.

Zenoo
  • 12,670
  • 4
  • 45
  • 69
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
  • 1
    Internet Explorer still alive ? Just Jock .. Ok so you need a [polyfill](http://stackoverflow.com/a/36810954/747579) to make `Array.from` works on Internet explorer – Abdennour TOUMI Mar 30 '17 at 18:00
  • 15
    According to MDN, calling [**Array.from()**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) `creates a new Array instance from an array-like or iterable object.` Creating a new array instance just to find an index might not be memory or GC efficient, depending on how frequent the operation, in which case iteration, as explained in the accepted answer, would be more ideal. – Chunky Chunk Jun 28 '17 at 07:30
  • 2
    @TheDarkIn1978 I am aware there is a trades-off between code elegance & app performance – Abdennour TOUMI Aug 26 '17 at 20:04
  • https://stackoverflow.com/a/76512374/14344959 – Harsh Patel Jun 20 '23 at 07:37
155

you can use the previousSibling property to iterate back through the siblings until you get back null and count how many siblings you've encountered:

var i = 0;
while( (child = child.previousSibling) != null ) 
  i++;
//at the end i will contain the index.

Please note that in languages like Java, there is a getPreviousSibling() function, however in JS this has become a property -- previousSibling.

Use previousElementSibling or nextElementSibling to ignore text and comment nodes.

Sadern Alwis
  • 104
  • 1
  • 4
  • 17
Liv
  • 6,006
  • 1
  • 22
  • 29
  • Sorry I just realised previousSibling in JavaScript is a property -- so there is no getPreviousSibling() function -- just updated the code. – Liv May 06 '11 at 16:04
  • 3
    Yep. You've left a getPreviousSibling() in the text though. – Tim Down May 06 '11 at 16:06
  • 7
    this approach requires the same number of iterations to determine the child index, so i can't see how it would be much faster. – Michael May 10 '13 at 21:04
  • It is faster because accessing nextSibling/previousSibling is much faster than accessing childNodes. – stroncium Jul 18 '14 at 18:30
  • 40
    One line version: `for (var i=0; (node=node.previousSibling); i++);` – Scott Miles Aug 29 '14 at 00:53
  • 2
    @sfarbota Javascript doesn't know block scoping, so `i` will be accesible. – A1rPun Sep 22 '14 at 15:12
  • @A1rPun, Right you are! I guess I've spent too much time in Java and C# lately. :) I deleted the misleading comment. – sfarbota Sep 22 '14 at 22:42
  • Which is faster, this answer or @KhalilRavanna's answer? – Jessica Feb 14 '16 at 20:41
  • It may also be worth knowing that if you have white space between elements (e.g. handwritten DOM), previousSibling will count those too. Some options for dealing with that here: https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM – Ben Frain Apr 29 '16 at 14:39
  • 1
    this does not actually work correctly - returns 1, 3, 5 instead of 0,1,2 as index for children. The answer from KhalilRavanna returns proper data. – nepdev May 14 '16 at 13:51
  • I supplied this answer some 5+ years ago, it's probably the case that this doesn't work anymore with today's browsers. – Liv May 16 '16 at 22:44
  • 7
    @nepdev That would be because of the differences between `.previousSibling` and `.previousElementSibling`. The former hits text nodes, the latter doesn't. – abluejelly May 23 '16 at 17:39
  • For those who use jQuery (like me): [.prev()](https://api.jquery.com/prev/) is a jQuery method to do this – Diego Somar Feb 22 '18 at 21:41
  • 2
    @ScottMiles Why not `var i=0; while(child=child.previousSibling) i++;`? – jak.b Jan 11 '19 at 19:42
65

ES—Shorter

[...element.parentNode.children].indexOf(element);

The spread Operator is a shortcut for that

philipp
  • 15,947
  • 15
  • 61
  • 106
13

I hypothesize that given an element where all of its children are ordered on the document sequentially, the fastest way should be to do a binary search, comparing the document positions of the elements. However, as introduced in the conclusion the hypothesis is rejected. The more elements you have, the greater the potential for performance. For example, if you had 256 elements, then (optimally) you would only need to check just 16 of them! For 65536, only 256! The performance grows to the power of 2! See more numbers/statistics. Visit Wikipedia

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentElement;
        if (!searchParent) return -1;
        var searchArray = searchParent.children,
            thisOffset = this.offsetTop,
            stop = searchArray.length,
            p = 0,
            delta = 0;
        
        while (searchArray[p] !== this) {
            if (searchArray[p] > this)
                stop = p + 1, p -= delta;
            delta = (stop - p) >>> 1;
            p += delta;
        }
        
        return p;
      }
    });
})(window.Element || Node);

Then, the way that you use it is by getting the 'parentIndex' property of any element. For example, check out the following demo.

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndex', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);

output.textContent = document.body.parentIndex;
output2.textContent = document.documentElement.parentIndex;
Body parentIndex is <b id="output"></b><br />
documentElements parentIndex is <b id="output2"></b>

Limitations

  • This implementation of the solution will not work in IE8 and below.

Binary VS Linear Search On 200,000 elements (might crash some mobile browsers, BEWARE!):

  • In this test, we will see how long it takes for a linear search to find the middle element VS a binary search. Why the middle element? Because it is at the average location of all the other locations, so it best represents all of the possible locations.

Binary Search

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexBinarySearch', {
      get: function() {
        var searchParent = this.parentNode;
        if (searchParent === null) return -1;
        var childElements = searchParent.children,
            lo = -1, mi, hi = childElements.length;
        while (1 + lo !== hi) {
            mi = (hi + lo) >> 1;
            if (!(this.compareDocumentPosition(childElements[mi]) & 0x2)) {
                hi = mi;
                continue;
            }
            lo = mi;
        }
        return childElements[hi] === this ? hi : -1;
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.9e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=200 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.9e+3+i+Math.random())).parentIndexBinarySearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the binary search ' + ((end-start)*10).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
}, 125);
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

Backwards (`lastIndexOf`) Linear Search

(function(t){"use strict";var e=Array.prototype.lastIndexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the backwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

Forwards (`indexOf`) Linear Search

(function(t){"use strict";var e=Array.prototype.indexOf;Object.defineProperty(t.prototype,"parentIndexLinearSearch",{get:function(){return e.call(t,this)}})})(window.Element||Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99e+3+i+Math.random())).parentIndexLinearSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the forwards linear search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

PreviousElementSibling Counter Search

Counts the number of PreviousElementSiblings to get the parentIndex.

(function(constructor){
   'use strict';
    Object.defineProperty(constructor.prototype, 'parentIndexSiblingSearch', {
      get: function() {
        var i = 0, cur = this;
        do {
            cur = cur.previousElementSibling;
            ++i;
        } while (cur !== null)
        return i; //Returns 3
      }
    });
})(window.Element || Node);
test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var child=test.children.item(99.95e+3);
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=100 + end; i-- !== end; )
    console.assert( test.children.item(
        Math.round(99.95e+3+i+Math.random())).parentIndexSiblingSearch );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the PreviousElementSibling search ' + ((end-start)*20).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden;white-space:pre></div>

No Search

For benchmarking what the result of the test would be if the browser optimized out the searching.

test.innerHTML = '<div> </div> '.repeat(200e+3);
// give it some time to think:
requestAnimationFrame(function(){
  var start=performance.now(), end=Math.round(Math.random());
  for (var i=2000 + end; i-- !== end; )
    console.assert( true );
  var end=performance.now();
  setTimeout(function(){
    output.textContent = 'It took the no search ' + (end-start).toFixed(2) + 'ms to find the 999 thousandth to 101 thousandth children in an element with 200 thousand children.';
    test.remove();
    test = null; // free up reference
  }, 125);
});
<output id=output> </output><br />
<div id=test style=visibility:hidden></div>

The Conculsion

However, after viewing the results in Chrome, the results are the opposite of what was expected. The dumber forwards linear search was a surprising 187 ms, 3850%, faster than the binary search. Evidently, Chrome somehow magically outsmarted the console.assert and optimized it away, or (more optimistically) Chrome internally uses numerical indexing system for the DOM, and this internal indexing system is exposed through the optimizations applied to Array.prototype.indexOf when used on a HTMLCollection object.

Jack G
  • 4,553
  • 2
  • 41
  • 50
  • Efficient, but impractical. – applemonkey496 May 06 '20 at 23:29
  • Talk about premature optimization. Sorry but this deserves a downvote... Why are you bothering with optimizing such simple lookup that's not often a source of bottlenecks? If you have nodes with thousands of children, you're probably doing it wrong. – Ruan Mendes Oct 05 '21 at 13:52
  • I guess the childNodes collection is implemented as a linked list in the engine, hence why binary search won't work efficiently. And that explains why `previousSibling` is a thing while `parentIndex` isn't. – Gokhan Kurt Oct 09 '21 at 21:22
10

Adding a (prefixed for safety) element.getParentIndex():

Element.prototype.PREFIXgetParentIndex = function() {
  return Array.prototype.indexOf.call(this.parentNode.children, this);
}
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
  • 2
    Theres a reason for web development's painfulness: prefix-jumpy dev's. Why not just do `if (!Element.prototype.getParentIndex) Element.prototype.getParentIndex = function(){ /* code here */ }`? Anyway, if this is ever implemented into the standard in the future then it will likely be implemented as a getter like `element.parentIndex`. So, I would say the best approach would be `if(!Element.prototype.getParentIndex) Element.prototype.getParentIndex=Element.prototype.parentIndex?function() {return this.parentIndex}:function() {return Array.prototype.indexOf.call(this.parentNode.children, this)}` – Jack G Jul 02 '17 at 20:27
  • 4
    Because the future `getParentIndex()` may have a different signature than your implementation. – mikemaccana Jul 04 '17 at 11:05
  • 3
    Skip the debate and just don't do prototype pollution. Nothing wrong with a plain old function. – mindplay.dk Apr 11 '21 at 13:19
  • [Pony fills](https://github.com/sindresorhus/ponyfill) are much safer than polluting code you do not own. `function getIndexFromParent(node){...}` – Ruan Mendes Oct 05 '21 at 13:46
  • @JuanMendes that’s true, if you’re happy with a function rather than a method, it’s highly unlikely the ECMA265 committee will add methods with your prefix. – mikemaccana Oct 05 '21 at 19:14
  • I'm not sure why anyone would offer such small convenience when it's [widely considered an anti-pattern](https://humanwhocodes.com/blog/2010/03/02/maintainable-javascript-dont-modify-objects-you-down-own/). Remember [Prototype js](http://prototypejs.org/) that was once so popular and died into oblivion? It's much easier to package and import/export a convenience function than it is tomodify the environment and be 100% sure you're not having unwanted side-effects. – Ruan Mendes Oct 05 '21 at 19:21
  • @JuanMendes the point is to understand why it’s considered an anti pattern (naming conflicts) and then see how that can be avoided. element.juanGetParentIndex() isn’t going to be in a new JS standard. – mikemaccana Oct 05 '21 at 19:44
  • And anyone who sees `node.blahblahGetParentIndex()` will be like "what????" and they will have to figure out how that method got added there. Whereas if it were a simple convenience function, it would be easy to see it imported into a file. You're not really buying a lot except for syntax sugar that's not even shorter. – Ruan Mendes Oct 05 '21 at 20:22
  • @JuanMendes if they work for a company called `blahblah` it's fairly obvious it's an internal extension. Speaking of antipatterns, the length of code is a really poor way of measuring it's understandability, that's why people avoid use single character variables. – mikemaccana Oct 07 '21 at 09:56
9

Could you do something like this:

var index = Array.prototype.slice.call(element.parentElement.children).indexOf(element);

https://developer.mozilla.org/en-US/docs/Web/API/Node/parentElement

1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231
7

If your element is <tr> or <td>, you can use the rowIndex/cellIndex property.

Jehong Ahn
  • 1,872
  • 1
  • 19
  • 25
  • 3
    note that in the case of `` elements, this will take into account any table headers you might also have in your table. – Dan O Nov 09 '22 at 15:51
4

Use binary search algorithm to improve the performace when the node has large quantity siblings.

function getChildrenIndex(ele){
    //IE use Element.sourceIndex
    if(ele.sourceIndex){
        var eles = ele.parentNode.children;
        var low = 0, high = eles.length-1, mid = 0;
        var esi = ele.sourceIndex, nsi;
        //use binary search algorithm
        while (low <= high) {
            mid = (low + high) >> 1;
            nsi = eles[mid].sourceIndex;
            if (nsi > esi) {
                high = mid - 1;
            } else if (nsi < esi) {
                low = mid + 1;
            } else {
                return mid;
            }
        }
    }
    //other browsers
    var i=0;
    while(ele = ele.previousElementSibling){
        i++;
    }
    return i;
}
cuixiping
  • 24,167
  • 8
  • 82
  • 93
  • Doesn't work. I'm compelled to point out that the IE version and "other browser" version will calculate different results. The "other browsers" technique works as expected, getting the nth position under the parent node, however the IE technique "Retrieves the ordinal position of the object, in source order, as the object appears in the document's all collection" ( http://msdn.microsoft.com/en-gb/library/ie/ms534635(v=vs.85).aspx ). E.g. I got 126 using "IE" technique, and then 4 using the other. – Christopher Bull Aug 14 '14 at 15:04
3

I had issue with text nodes, and it was showing wrong index. Here is version to fix it.

function getChildNodeIndex(elem)
{   
    let position = 0;
    while ((elem = elem.previousSibling) != null)
    {
        if(elem.nodeType != Node.TEXT_NODE)
            position++;
    }

    return position;
}
0
Object.defineProperties(Element.prototype,{
group : {
    value: function (str, context) {
        // str is valid css selector like :not([attr_name]) or .class_name
        var t = "to_select_siblings___";
        var parent = context ? context : this.parentNode;
        parent.setAttribute(t, '');
        var rez = document.querySelectorAll("[" + t + "] " + (context ? '' : ">") + this.nodeName + (str || "")).toArray();
        parent.removeAttribute(t);            
        return rez;  
    }
},
siblings: {
    value: function (str, context) {
        var rez=this.group(str,context);
        rez.splice(rez.indexOf(this), 1);
        return rez; 
    }
},
nth: {  
    value: function(str,context){
       return this.group(str,context).indexOf(this);
    }
}
}

Ex

/* html */
<ul id="the_ul">   <li></li> ....<li><li>....<li></li>   </ul>

 /*js*/
 the_ul.addEventListener("click",
    function(ev){
       var foo=ev.target;
       foo.setAttribute("active",true);
       foo.siblings().map(function(elm){elm.removeAttribute("active")});
       alert("a click on li" + foo.nth());
     });
bortunac
  • 4,642
  • 1
  • 32
  • 21
  • 1
    Can you explain why you extend from `Element.prototype`? The functions look useful but I don't know what these functions do (even if the namings are obvious). – A1rPun Dec 04 '15 at 19:04
  • @ extend Element.prototype the reason is similarity ... 4 ex elemen.children , element.parentNode etc ... so the same way you address element.siblings .... the group method is a little complicated cause I want to extend a little the sibling approach to elelemts alike by the same nodeType and having same attributes even not having same ancestor – bortunac Dec 04 '15 at 19:12
  • I know what prototype extending is but I like to know how is your code used. `el.group.value()` ??. My first comment is there to improve the quality of your answer. – A1rPun Dec 04 '15 at 19:36
  • the group and siblings methods return Array with founded dom elements .. .... thanks for your comment and for the reason of comment – bortunac Dec 04 '15 at 19:44
  • Very elegant, yet also very slow. – Jack G Jul 02 '17 at 22:37
0

thanks to the answer by @Liv

now i always use,

Node.prototype.index=function() {
    nc=this;
    ind=-1;
    while(nc!=null){
      nc=nc.previousSibling
      ind++;
    }
    return ind
  }

on my codes,

usage:

var child = document.getElementById('my_element');

child.index();

Element.index() returns an integer first child is 0

ANDuser
  • 71
  • 8
-1

For me this code is more clear

const myElement = ...;
const index = [...document.body.children].indexOf(myElement);
  • How is this any different from [philipp](https://stackoverflow.com/a/42692428/369450)'s answer? You're creating an array from the children and finding the index. – Uyghur Lives Matter Aug 25 '21 at 15:47
-2
<body>
    <section>
        <section onclick="childIndex(this)">child a</section>
        <section onclick="childIndex(this)">child b</section>
        <section onclick="childIndex(this)">child c</section>
    </section>
    <script>
        function childIndex(e){
            let i = 0;
            while (e.parentNode.children[i] != e) i++;
            alert('child index '+i);
        }
    </script>
</body>
ofir_aghai
  • 3,017
  • 1
  • 37
  • 43