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.
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
childrenandparentElement. 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
firstChildornextSiblingto be an element. They often return blank text nodes. UsefirstElementChildandnextElementSibling. -
Not handling
nullat the edges.last.nextElementSibling.textContentcrashes, 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
forEachormaponchildren. It is an HTMLCollection, not an array. Usefor...of, orArray.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. Useclosest(".card")instead. -
Confusing
parentNodeandparentElement. They only differ at the top of the page.parentElementis the safer choice. -
Forgetting that a text node has no
children. Properties likechildrenandfirstElementChildexist only on elements. -
Thinking
childNodes.lengthequals the number of tags. It also counts text and comment nodes.
Practice
- Select
#currentand print its parent’sid, and theidof its grandparent. - Print how many items are in
tasks.childrenand intasks.childNodes. Explain the difference in your own words. - Print the text of the first and last task with
firstElementChildandlastElementChild. - Print the text of the task before and after
#current. What do you get forpreviousElementSiblingof the first task? - Write a
whileloop that goes from#currentup to<html>and prints every tag name. - Write a loop that prints all four tasks using
firstElementChildandnextElementSibling. - Build a
siblingsarray for#current(without#currentitself) and print the text of each sibling. - Use
closest()to find thediv#appfrom#current. Then testcurrent.closest(".missing")and handle thenullresult. - Try the FAQ page. Add a fourth question and answer. Does the script need any change? Why or why not?
- Challenge: in the to-do page, change the delete code so it reads the task text with
closest("li")andquerySelector("span")instead ofpreviousElementSibling. 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(orparentNode). Useclosest("selector")to find the nearest ancestor that matches, even when it is deep above. - Go down with
children(elements only),childNodes(all nodes),firstElementChildandlastElementChild. - Go sideways with
previousElementSiblingandnextElementSibling. There is no built-insiblings, but you can build one fromparent.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?.. childrenandchildNodesare live. Copy them withArray.from()before you add or remove items in a loop.matches()andcontains()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,removeAttributeandhasAttribute.