JavaScriptIntermediate

JavaScript DOM Traversal: parent, children and siblings Explained

Move around the DOM tree with parentElement, children, childNodes, firstElementChild and sibling properties, using real examples like FAQs and to-do lists.

All JavaScript lessons

What you will learn

Often you already have one element and you need one of its neighbours. A user clicks a “Delete” button and you must remove the whole to-do item that holds it. A user clicks a question and you must show the answer that comes right after it. Moving from one element to the elements around it is called DOM traversal. In this lesson you will learn how to go to the parent, the children and the siblings of an element with parentElement, children, childNodes, firstElementChild, lastElementChild and the sibling properties, plus some handy helpers like closest().

The family tree idea

The DOM is a tree, and trees have family relationships. We use the same words you use for your own family:

<div id="app">
  <h2 id="heading">My Study Plan</h2>
  <ul id="tasks">
    <li class="task done">Learn HTML</li>
    <li class="task" id="current">Learn CSS</li>
    <li class="task">Learn JavaScript</li>
    <li class="task">Build a project</li>
  </ul>
  <p id="note">Keep going!</p>
</div>
div#app
├── h2#heading
├── ul#tasks
│   ├── li.task.done     "Learn HTML"
│   ├── li#current       "Learn CSS"
│   ├── li.task          "Learn JavaScript"
│   └── li.task          "Build a project"
└── p#note

Using li#current (“Learn CSS”) as the starting point:

Relationship Who Property you will use
Parent ul#tasks parentElement
Grandparent div#app parentElement.parentElement
Siblings The other three li items previousElementSibling, nextElementSibling
Children None (only a text node inside) children

And from the ul#tasks point of view, the four li items are its children, and the first one is its firstElementChild.

All the code in this lesson runs on this page, which is also loaded in Practice in Editor. Start every example with this:

const current = document.getElementById("current");
const tasks = document.getElementById("tasks");

Going up: parents

parentElement

Gives the parent element. If there is no parent element, it gives null.

console.log(current.parentElement);             // <ul id="tasks">...</ul>
console.log(current.parentElement.id);          // "tasks"
console.log(current.parentElement.parentElement.id);   // "app" (the grandparent)

You can chain it to climb higher, but do not climb too far. Long chains like parentElement.parentElement.parentElement break easily if someone changes the HTML. A better tool, closest(), is shown below.

parentNode

parentNode gives the parent node. For almost every element it is the same as parentElement. The only common difference is at the very top:

console.log(document.documentElement.parentNode);      // #document (a node, but not an element)
console.log(document.documentElement.parentElement);   // null (no parent element)

Use parentElement when you want an element, which is nearly always.

closest(): the nearest ancestor

closest("selector") starts at the element itself, then checks its parent, then the grandparent, and so on, until one matches the CSS selector. It returns that element, or null if none matches.

console.log(current.closest("ul").id);      // "tasks"
console.log(current.closest("#app").tagName);   // "DIV"
console.log(current.closest(".task") === current);   // true (it checks the element itself first)
console.log(current.closest(".nothing"));   // null

This is the safest way to say “find the card (or row, or item) that contains this thing”, no matter how deep the thing is.

Going down: children

children

An HTMLCollection with only the child elements. This is the one you will use most.

console.log(tasks.children.length);              // 4
console.log(tasks.children[1].textContent);      // "Learn CSS"
console.log(tasks.children[0].textContent);      // "Learn HTML"

childNodes

A NodeList with all child nodes: elements, text and comments.

console.log(tasks.childNodes.length);   // 9

Why 9 and not 4? Remember from the DOM introduction that line breaks and spaces between tags become text nodes. Inside the ul there are 4 li elements and 5 blank-space text nodes (before the first li, between each pair, and after the last). The numbers match the HTML as written in the sample above. If you remove the spaces and line breaks between the tags, childNodes.length would become 4.

ul#tasks.childNodes
#text  (line break + spaces)
li     Learn HTML
#text
li     Learn CSS
#text
li     Learn JavaScript
#text
li     Build a project
#text  (line break + spaces)

That is why developers prefer the “Element” versions of the properties.

firstElementChild and lastElementChild

The first and last child element. They skip text nodes.

console.log(tasks.firstElementChild.textContent);   // "Learn HTML"
console.log(tasks.lastElementChild.textContent);    // "Build a project"

firstChild and lastChild

The first and last child node, which can be a text node.

console.log(tasks.firstChild.nodeName);   // "#text" (the line break before the first li)
console.log(tasks.lastChild.nodeName);    // "#text"

This is a very common surprise. If you expected firstChild to be the first li, you will get a blank text node instead.

More helpers

console.log(tasks.childElementCount);   // 4 (same as children.length)
console.log(tasks.hasChildNodes());     // true
console.log(document.getElementById("note").children.length);   // 0 (only text inside)

Going sideways: siblings

Siblings are elements that share the same parent.

nextElementSibling and previousElementSibling

console.log(current.previousElementSibling.textContent);   // "Learn HTML"
console.log(current.nextElementSibling.textContent);       // "Learn JavaScript"

At the edges there is nothing to find, so you get null:

const first = tasks.firstElementChild;
const last = tasks.lastElementChild;

console.log(first.previousElementSibling);   // null (nothing before the first)
console.log(last.nextElementSibling);        // null (nothing after the last)

nextSibling and previousSibling

The same idea, but for nodes, so you often get blank text nodes:

console.log(current.nextSibling.nodeName);       // "#text"
console.log(current.previousSibling.nodeName);   // "#text"

Use the Element versions unless you really need text nodes.

Get all siblings

There is no siblings property, but it is easy to build one. Take the parent’s children and remove the element itself:

const siblings = Array.from(current.parentElement.children).filter(function (el) {
  return el !== current;
});

console.log(siblings.map(function (el) { return el.textContent; }));
// ["Learn HTML", "Learn JavaScript", "Build a project"]

All properties at a glance

Property Returns Includes text nodes? When there is nothing
parentElement Parent element No null
parentNode Parent node Not applicable null
children HTMLCollection of child elements No Empty collection
childNodes NodeList of all child nodes Yes Empty NodeList
firstElementChild First child element No null
lastElementChild Last child element No null
firstChild First child node Yes null
lastChild Last child node Yes null
previousElementSibling Previous sibling element No null
nextElementSibling Next sibling element No null
previousSibling Previous sibling node Yes null
nextSibling Next sibling node Yes null

Tip: Your default choice should be the properties with Element in the name, plus children and parentElement. They skip the blank text nodes and are what you usually mean.

Walking with loops

Because every step gives you the next element (or null), you can walk through the tree with a while loop. The loop stops when you reach null.

Walk up to the top

let el = current;

while (el) {
  console.log(el.tagName);
  el = el.parentElement;
}

Output:

LI
UL
DIV
BODY
HTML

Walk across the siblings

let task = tasks.firstElementChild;

while (task) {
  console.log(task.textContent);
  task = task.nextElementSibling;
}

Output:

Learn HTML
Learn CSS
Learn JavaScript
Build a project

(For simple cases, for (const task of tasks.children) does the same job with less code.)

Real use cases

1. Course progress: done, current and upcoming

Many learning sites show finished lessons in green, the current lesson in bold and the next ones in grey. Starting from the current item, you can walk both ways with the sibling properties:

let before = current.previousElementSibling;
while (before) {
  before.style.color = "green";
  before = before.previousElementSibling;
}

let after = current.nextElementSibling;
while (after) {
  after.style.color = "gray";
  after = after.nextElementSibling;
}

current.style.fontWeight = "bold";

Run it on the practice page and you will see “Learn HTML” in green, “Learn CSS” in bold, and the last two in grey.

2. FAQ: show the answer after the question

Each question button is followed by its answer. From the clicked button, nextElementSibling is the answer, so one small piece of code works for any number of questions. This example uses addEventListener, which you will learn properly in the events lessons. For now, read it as “when this button is clicked, run this function”.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>FAQ</title>
  <style>
    body { font-family: sans-serif; max-width: 480px; margin: 20px auto; }
    .question {
      display: block;
      width: 100%;
      text-align: left;
      padding: 12px;
      font-size: 16px;
      border: 1px solid #ccc;
      background: #f3f4f6;
      cursor: pointer;
    }
    .answer { margin: 0 0 10px; padding: 12px; border: 1px solid #ccc; border-top: none; }
  </style>
</head>
<body>
  <h2>FAQ</h2>

  <button class="question">Is JavaScript free to learn?</button>
  <p class="answer" hidden>Yes. You only need a browser and a text editor.</p>

  <button class="question">Do I need to learn HTML first?</button>
  <p class="answer" hidden>Yes. Knowing HTML and CSS makes JavaScript much easier.</p>

  <button class="question">How long does it take?</button>
  <p class="answer" hidden>A few months of regular practice is enough to build real projects.</p>

  <script>
    const questions = document.querySelectorAll(".question");

    questions.forEach(function (question) {
      question.addEventListener("click", function () {
        const answer = question.nextElementSibling;   // the <p> right after the button
        answer.hidden = !answer.hidden;               // show it, or hide it again
      });
    });
  </script>
</body>
</html>

The line question.nextElementSibling is the whole idea of this lesson in one line: start from the element you have, and move to its neighbour.

3. To-do list: delete the whole item

A “Delete” button sits inside its li. To delete the item, you must go from the button to its parent. We use closest("li") because it still works if the button is later placed inside another wrapper.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>To-do</title>
  <style>
    body { font-family: sans-serif; max-width: 400px; margin: 20px auto; }
    ul { padding: 0; list-style: none; }
    li {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px;
      border-bottom: 1px solid #ddd;
    }
    .delete { color: white; background: #dc2626; border: none; border-radius: 4px; padding: 4px 10px; cursor: pointer; }
  </style>
</head>
<body>
  <h2>My Tasks</h2>
  <ul id="todo">
    <li><span>Buy milk</span> <button class="delete">Delete</button></li>
    <li><span>Finish homework</span> <button class="delete">Delete</button></li>
    <li><span>Practice JavaScript</span> <button class="delete">Delete</button></li>
  </ul>

  <script>
    const buttons = document.querySelectorAll(".delete");

    buttons.forEach(function (button) {
      button.addEventListener("click", function () {
        const item = button.closest("li");                 // go up to the <li>
        const text = button.previousElementSibling.textContent;   // go sideways to the <span>
        console.log("Deleted:", text);
        item.remove();                                     // remove the whole item
      });
    });
  </script>
</body>
</html>

This one example uses two kinds of traversal: closest("li") to go up, and previousElementSibling to go sideways. (remove() is explained in the lesson on creating and removing elements.)

More helpers: matches() and contains()

Two small methods that go well with traversal:

Method Question it answers
element.matches("selector") Does this element match this CSS selector? (true or false)
parent.contains(other) Is other inside parent? (true or false)
console.log(current.matches(".task"));     // true
console.log(current.matches(".done"));     // false

const app = document.getElementById("app");
console.log(app.contains(current));        // true
console.log(current.contains(app));        // false

Careful: children is a live collection

children (and childNodes) are live, which means they change when the page changes. This causes a famous bug when you remove elements inside a loop:

// Bug: this removes only every second item!
const items = tasks.children;

for (let i = 0; i < items.length; i++) {
  items[i].remove();
}

Why? After you remove item 0, the old item 1 moves into position 0, but i moves on to 1. The loop skips an item each time. Two safe fixes:

// Fix 1: copy the live list into a normal array first
Array.from(tasks.children).forEach(function (item) {
  item.remove();
});

// Fix 2: keep removing the first child until none is left
while (tasks.firstElementChild) {
  tasks.firstElementChild.remove();
}

(You do not need to run these now. Just remember: never change a live list while looping over it directly.)

Common mistakes

  • Expecting firstChild or nextSibling to be an element. They often return blank text nodes. Use firstElementChild and nextElementSibling.

  • Not handling null at the edges. last.nextElementSibling.textContent crashes, because the last item has no next sibling. Check first, or use ?.:

    const nextText = last.nextElementSibling?.textContent ?? "No more tasks";
    console.log(nextText);   // "No more tasks"
  • Calling forEach or map on children. It is an HTMLCollection, not an array. Use for...of, or Array.from(children).

  • Changing a live list while looping. Copy it with Array.from() first.

  • Long chains like parentElement.parentElement.parentElement. They break when the HTML changes. Use closest(".card") instead.

  • Confusing parentNode and parentElement. They only differ at the top of the page. parentElement is the safer choice.

  • Forgetting that a text node has no children. Properties like children and firstElementChild exist only on elements.

  • Thinking childNodes.length equals the number of tags. It also counts text and comment nodes.

Practice

  1. Select #current and print its parent’s id, and the id of its grandparent.
  2. Print how many items are in tasks.children and in tasks.childNodes. Explain the difference in your own words.
  3. Print the text of the first and last task with firstElementChild and lastElementChild.
  4. Print the text of the task before and after #current. What do you get for previousElementSibling of the first task?
  5. Write a while loop that goes from #current up to <html> and prints every tag name.
  6. Write a loop that prints all four tasks using firstElementChild and nextElementSibling.
  7. Build a siblings array for #current (without #current itself) and print the text of each sibling.
  8. Use closest() to find the div#app from #current. Then test current.closest(".missing") and handle the null result.
  9. Try the FAQ page. Add a fourth question and answer. Does the script need any change? Why or why not?
  10. Challenge: in the to-do page, change the delete code so it reads the task text with closest("li") and querySelector("span") instead of previousElementSibling. Both ways should log the same text. Which one would still work if you moved the <span> after the button?

Recap

  • DOM traversal means moving from one element to related elements: parent, children and siblings.
  • Go up with parentElement (or parentNode). Use closest("selector") to find the nearest ancestor that matches, even when it is deep above.
  • Go down with children (elements only), childNodes (all nodes), firstElementChild and lastElementChild.
  • Go sideways with previousElementSibling and nextElementSibling. There is no built-in siblings, but you can build one from parent.children.
  • Properties without “Element” in the name (firstChild, nextSibling, childNodes) include text nodes, such as the blank space between tags.
  • At the edges you get null, so check it or use ?..
  • children and childNodes are live. Copy them with Array.from() before you add or remove items in a loop.
  • matches() and contains() answer true/false questions about an element.
  • Real projects use traversal everywhere: FAQs, accordions, delete buttons, menus and progress lists.
  • Next you will learn how to read and change an element’s attributes with getAttribute, setAttribute, removeAttribute and hasAttribute.