In short
DOM navigation lets you move from one element to the elements around it. Learn parentElement, children, nextElementSibling, closest, nodes vs elements, and live vs static lists. What is DOM navigation? In the previous lesson you found elements with querySelector.
DOM navigation lets you move from one element to the elements around it. Learn parentElement, children, nextElementSibling, closest, nodes vs elements, and live vs static lists.
What is DOM navigation?
In the previous lesson you found elements with querySelector. Often, though, you already have one element and need the one next to it: the box around a button that was clicked, the answer under a question, or the next item in a list. DOM navigation (also called DOM traversal) means moving from one element to its relatives in the DOM tree.
The DOM tree works like a family tree, and it uses family words:
- The parent is the element directly around another element.
- Children are the elements directly inside an element.
- Siblings are elements with the same parent, side by side.
- Ancestors are the parent, the parent's parent, and so on up to
<html>. Descendants are children, grandchildren and so on.
We will use this product card in the examples:
<article class="card" id="card">
<h2 class="card-title">Wireless headphones</h2>
<p class="price">$89</p>
<ul class="features">
<li>40-hour battery</li>
<li class="highlight">Noise cancelling</li>
<li>Foldable</li>
</ul>
<button class="buy">Add to cart</button>
</article>
Here <article> is the parent of <h2>, <p>, <ul> and <button>. Those four are siblings of each other. The three <li> elements are children of the <ul> and descendants of the <article>.
Moving up, down and sideways: the navigation properties
Every element has properties that point to its relatives. They return null when there is no such relative, for example when you ask for the next sibling of the last child.
| Property | Direction | Returns |
|---|---|---|
parentElement | Up | The parent element |
children | Down | All child elements (an HTMLCollection) |
firstElementChild | Down | The first child element |
lastElementChild | Down | The last child element |
childElementCount | Down | How many child elements there are |
nextElementSibling | Sideways | The next sibling element |
previousElementSibling | Sideways | The previous sibling element |
closest(selector) | Up | The nearest ancestor (or the element itself) that matches |
Put the card HTML in a page and try these in a script or the console:
const highlight = document.querySelector(".highlight");
console.log(highlight.textContent); // Output: Noise cancelling
console.log(highlight.parentElement.className); // Output: features
console.log(highlight.previousElementSibling.textContent); // Output: 40-hour battery
console.log(highlight.nextElementSibling.textContent); // Output: Foldable
console.log(highlight.nextElementSibling.nextElementSibling); // Output: null
const card = document.querySelector("#card");
console.log(card.childElementCount); // Output: 4
console.log(card.firstElementChild.textContent); // Output: Wireless headphones
console.log(card.lastElementChild.textContent); // Output: Add to cart
for (const child of card.children) {
console.log(child.tagName);
}
// Output: H2, P, UL, BUTTON (one per line)
You can chain these properties, like highlight.parentElement.parentElement to reach the card. But long chains break easily: if someone adds a wrapper <div> to the HTML, the chain points to the wrong element. That is where closest helps.
Finding the right ancestor with closest()
You met closest() briefly in the Event Delegation lesson. element.closest(selector) starts at the element itself and walks up through its ancestors, returning the first one that matches the CSS selector, or null if none do. It does not care how many levels up the match is, so it keeps working when the HTML changes.
const buyButton = document.querySelector(".buy");
const productCard = buyButton.closest(".card");
console.log(productCard.id); // Output: card
console.log(productCard.querySelector(".card-title").textContent); // Output: Wireless headphones
console.log(buyButton.closest("button") === buyButton); // Output: true (it checks itself first)
console.log(buyButton.closest(".sidebar")); // Output: null
A very common pattern is "go up with closest, then down with querySelector". From any button in a card, you climb to the card, then search inside it for the title or the price.
Here is a full example: an FAQ accordion where each question button shows or hides the answer right after it, using nextElementSibling:
<!DOCTYPE html>
<html lang="en">
<body>
<div class="faq">
<button class="question">How long is delivery?</button>
<p class="answer" hidden>Usually 2 to 4 working days.</p>
<button class="question">Can I return an item?</button>
<p class="answer" hidden>Yes, within 30 days.</p>
</div>
<script>
const faq = document.querySelector(".faq");
faq.addEventListener("click", (event) => {
const question = event.target.closest(".question");
if (!question) return; // the click was not on a question
const answer = question.nextElementSibling; // the <p> right after the button
answer.hidden = !answer.hidden; // show or hide it
console.log(`${question.textContent} -> ${answer.hidden ? "closed" : "open"}`);
});
</script>
</body>
</html>
How long is delivery? -> open
Click a question and its answer appears; click again to hide it. There is no ID on any answer: each button simply finds its neighbour.
Nodes vs elements: why firstChild gives you text
The DOM contains more than elements. Everything in it is a node: elements are nodes, but so are pieces of text and comments. Even the spaces and line breaks between your tags become text nodes.
That is why there are two sets of navigation properties. The ones with "Element" in the name skip text and comments. The older ones include every node:
| Elements only (use these) | All nodes, including text |
|---|---|
children | childNodes |
firstElementChild / lastElementChild | firstChild / lastChild |
nextElementSibling / previousElementSibling | nextSibling / previousSibling |
parentElement | parentNode |
const features = document.querySelector(".features");
console.log(features.children.length); // Output: 3 (the three <li> elements)
console.log(features.childNodes.length); // Output: 7 (3 <li> plus 4 whitespace text nodes)
console.log(features.firstChild.nodeName); // Output: #text
console.log(features.firstElementChild.nodeName); // Output: LI
The 4 extra nodes are the line breaks and spaces before, between and after the <li> tags.
Common mistake: Using
nextSiblingorfirstChildand getting a text node full of spaces instead of the element you wanted. Unless you really need text nodes, always use the "Element" versions.
HTMLCollection vs NodeList: live vs static lists
Different methods give you different kinds of lists:
| You get it from | Type | Contains | Live or static? |
|---|---|---|---|
element.children | HTMLCollection | Elements | Live |
getElementsByClassName(), getElementsByTagName() | HTMLCollection | Elements | Live |
element.childNodes | NodeList | All nodes | Live |
querySelectorAll() | NodeList | Elements | Static |
A live list updates itself automatically when the page changes. A static list is a snapshot: it stays exactly as it was when you created it.
const list = document.querySelector(".features");
const liveItems = list.children; // live HTMLCollection
const snapshot = list.querySelectorAll("li"); // static NodeList
const newItem = document.createElement("li");
newItem.textContent = "Bluetooth 5.3";
list.append(newItem);
console.log(liveItems.length); // Output: 4 (updated by itself)
console.log(snapshot.length); // Output: 3 (still the old snapshot)
Another difference: a NodeList has a forEach method, but an HTMLCollection does not. Both work with for...of, and you can turn either into a real array with Array.from(list) or [...list] to use map and filter.
Tip: Live lists can surprise you inside loops. If you remove elements while looping over a live list, its length shrinks as you go and you skip items. Copy it first with
Array.from(), or usequerySelectorAll.
Practice exercise
Build a small to-do list with three <li> items, each containing a task name in a <span> and two buttons, "Up" and "Down". Use one click listener on the <ul>. When "Up" is clicked, move that <li> above its previous sibling; when "Down" is clicked, move it below its next sibling. Do nothing at the top or bottom of the list.
Hint: find the item with event.target.closest("li"). Use item.previousElementSibling and prev.before(item) to move up, and item.nextElementSibling with next.after(item) to move down. Check for null first.
Key takeaways
parentElementgoes up,children,firstElementChildandlastElementChildgo down, andnextElementSiblingandpreviousElementSiblinggo sideways.closest(selector)finds the nearest matching ancestor and survives changes to the HTML better than long chains.- Text and whitespace are nodes too; use the properties with "Element" in their names to skip them.
childrenis a live HTMLCollection;querySelectorAllgives a static NodeList snapshot.