DOM Traversal: Navigating the Family Tree
🧭 Finding Your Way Around a Web Page
Imagine you’re standing in a hallway of a large building. You can see the room you’re in, but you also know there are rooms above you (upstairs), rooms next to you (neighbours), and rooms inside this room (closets, alcoves). If someone says ‘go to the room next door’, you don’t need to walk all the way back to the front entrance and start a new search — you just step sideways.
The DOM works the same way. Once you’ve selected an element, you can walk from it to its parent, its children, or its siblings without going back to document.querySelector(). This is called DOM traversal, and it’s one of the most powerful techniques for writing efficient, readable JavaScript.
By the end of this lesson you’ll be able to navigate any web page’s structure using just a handful of built-in properties.
Learning Objectives
By the end of this lesson, you'll be able to:
- ✓ Explain Explain the parent–child–sibling relationships in the DOM tree
- ✓ Use parentNode and parentElement to move up the tree
- ✓ Use children, firstElementChild, and lastElementChild to move down
- ✓ Use nextElementSibling and previousElementSibling to move sideways
- ✓ Explain Explain the difference between Node properties (childNodes) and Element properties (children)
- ✓ Combine traversal methods to reach any element from any starting point
- ✓ Apply Apply DOM traversal in a real-world scenario instead of querying from document every time
Why This Matters:
DOM traversal lets you write shorter, faster code by walking between related elements instead of repeatedly searching the entire document.
The DOM Family Tree
Every element in the DOM has relationships with other elements, just like people in a family tree. The language we use is the same:
- Parent — the element that directly contains this element
- Children — the elements directly inside this element
- Siblings — elements that share the same parent
- Ancestors — all elements above this one (parent, grandparent, …)
- Descendants — all elements below this one (children, grandchildren, …)
Consider this HTML:
<body>
<header>
<h1>My Site</h1>
<nav>
<a href="#">Home</a>
<a href="#">About</a>
</nav>
</header>
<main>
<section>
<h2>Welcome</h2>
<p>Hello world</p>
</section>
<article>
<h2>News</h2>
<p>Latest post</p>
</article>
</main>
<footer>
<p>© 2025</p>
</footer>
</body>In this structure:
<body>is the parent of<header>,<main>, and<footer><header>,<main>, and<footer>are siblings of each other<section>and<article>are children of<main><h2>inside<section>is a grandchild (descendant) of<main>
Moving Up: Parent Properties
To move up the tree from an element to the element that contains it, use:
const section = document.querySelector('section');
// parentElement — always returns an element (or null)
section.parentElement; // <main>
// parentNode — may return a non-element node (rare edge case)
section.parentNode; // <main> (same in this case) Which should you use? Prefer parentElement. It guarantees you get an element back (or null if you’ve reached the top). parentNode can return the document node above <html>, which is rarely what you want.
Climbing Multiple Levels
You can chain parentElement to climb several levels at once:
const h2 = document.querySelector('section h2');
h2.parentElement; // <section>
h2.parentElement.parentElement; // <main>
h2.parentElement.parentElement.parentElement; // <body>closest() — The Smart Climber
Instead of chaining parentElement repeatedly, use closest() to jump straight to the nearest ancestor that matches a CSS selector:
const h2 = document.querySelector('section h2');
// Find the nearest ancestor that is a <main>
h2.closest('main'); // <main>
// Find the nearest ancestor with a class
h2.closest('.container'); // null (none found)
// closest() checks the element itself too
h2.closest('h2'); // the h2 itself Real-world use: closest() is incredibly useful in event handling. When a user clicks a button inside a card, you can use event.target.closest('.card') to find the card that was clicked, no matter how deeply nested the button is.
Moving Down: Child Properties
To move down the tree from a parent to its contents:
| Property | Returns | Includes text nodes? |
|---|---|---|
children | HTMLCollection of child elements | No |
childNodes | NodeList of all child nodes | Yes |
firstElementChild | First child element | No |
lastElementChild | Last child element | No |
firstChild | First child node (any type) | Yes |
lastChild | Last child node (any type) | Yes |
const mainEl = document.querySelector('main');
// children — only element nodes (what you usually want)
mainEl.children; // [section, article]
mainEl.children.length; // 2
mainEl.children[0]; // <section>
// First and last shortcuts
mainEl.firstElementChild; // <section>
mainEl.lastElementChild; // <article>
// childNodes includes whitespace text nodes!
mainEl.childNodes; // [text, section, text, article, text]
mainEl.childNodes.length; // 5 (three text nodes for whitespace) Common trap: childNodes includes invisible whitespace text nodes (the line breaks between your HTML tags). This is the number-one gotcha for beginners. Almost always use children instead.
Looping Through Children
const nav = document.querySelector('nav');
// children returns an HTMLCollection — use for...of
for (const link of nav.children) {
console.log(link.textContent);
}
// Or convert to an array for .forEach, .map, .filter
const linksArray = Array.from(nav.children);
linksArray.forEach(link => {
link.classList.add('nav-link');
});⏸️ Check Your Understanding: Parents & Children
Before moving forward, can you answer these?
- What is the difference between parentNode and parentElement?
- You use mainEl.childNodes.length and get 5, but there are only 2 child elements. Why?
- How would you get all child elements of a <nav> and add a class to each one?
Check Your Answers
- parentElement always returns an element node (or null at the top of the tree). parentNode can return a non-element node like the document node above <html>. In practice, parentElement is almost always what you want.
- childNodes includes all node types, including invisible whitespace text nodes (the line breaks and spaces between your HTML tags). Use children instead — it returns only element nodes.
- Use Array.from(nav.children).forEach(child => child.classList.add("nav-item")). You need Array.from() because children returns an HTMLCollection, not an array.
How confident are you with this concept?
😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!
Moving Sideways: Sibling Properties
Siblings are elements that share the same parent. You can step from one sibling to the next or previous:
const mainEl = document.querySelector('main');
// Previous sibling element
mainEl.previousElementSibling; // <header>
// Next sibling element
mainEl.nextElementSibling; // <footer>
// At the edges, you get null
const header = document.querySelector('header');
header.previousElementSibling; // null (no sibling before it) Element vs Node: Just like with children, always use the Element versions (nextElementSibling, previousElementSibling). The plain nextSibling / previousSibling may return whitespace text nodes.
Walking All Siblings
There is no built-in “give me all siblings” property, but you can get them by going up to the parent and then back down to its children:
const mainEl = document.querySelector('main');
// Get all siblings (including mainEl itself)
const allSiblings = mainEl.parentElement.children;
// [header, main, footer]
// Get only the OTHER siblings (exclude mainEl)
const otherSiblings = Array.from(allSiblings)
.filter(el => el !== mainEl);
// [header, footer]Nodes vs Elements: Why It Matters
This is the most confusing part of DOM traversal for beginners. The DOM has two parallel sets of traversal properties:
| Node properties (all nodes) | Element properties (elements only) |
|---|---|
parentNode | parentElement |
childNodes | children |
firstChild | firstElementChild |
lastChild | lastElementChild |
nextSibling | nextElementSibling |
previousSibling | previousElementSibling |
Node properties include everything — elements, text nodes (including whitespace), and comments. Element properties skip all non-element nodes and give you only the HTML tags.
// Given this HTML: <ul> <li>A</li> <li>B</li> </ul>
const ul = document.querySelector('ul');
// Node version — includes whitespace text nodes!
ul.firstChild; // #text (the whitespace before first <li>)
ul.childNodes.length; // 5 (text, li, text, li, text)
// Element version — only elements, what you actually want
ul.firstElementChild; // <li>A</li>
ul.children.length; // 2 (li, li)Rule of thumb: Always use the Element versions unless you have a specific reason to work with text nodes or comments. This will save you hours of debugging.
⏸️ Check Your Understanding: Siblings & Nodes vs Elements
Before moving forward, can you answer these?
- How do you get the element immediately after <main> in the DOM?
- Why should you always prefer the "Element" versions of traversal properties?
- There is no built-in "siblings" property. How can you get all siblings of an element?
Check Your Answers
- Use mainEl.nextElementSibling. This skips text nodes and returns the next element, which would be <footer> in our example HTML.
- The plain Node versions (nextSibling, firstChild, etc.) include whitespace text nodes and comments, which are almost never what you want. The Element versions (nextElementSibling, firstElementChild, etc.) skip those and give you only HTML elements.
- Go up to the parent and then back down: Array.from(element.parentElement.children).filter(el => el !== element). This gives you all children of the parent except the element itself.
How confident are you with this concept?
😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!
Combining Traversal Methods
The real power of traversal is chaining multiple steps together to reach any element from any starting point:
const section = document.querySelector('section');
// From section, get the first link in the header
section
.parentElement // <main>
.previousElementSibling // <header>
.querySelector('a'); // first <a> in header
// From article, get the h2 inside its sibling section
const article = document.querySelector('article');
article
.previousElementSibling // <section>
.firstElementChild; // <h2>Welcome</h2>Traversal vs querySelector — When to Use Which
| Use traversal when… | Use querySelector when… |
|---|---|
| You already have a reference to a nearby element | You need to find an element anywhere in the page |
| You’re inside an event handler and need the parent card/row | You’re setting up the page and need initial references |
| The relationship is structural (parent, next sibling) | The target has a unique ID or class |
| You want to avoid searching the whole document again | The elements aren’t in a predictable structural relationship |
Real-World Example: Accordion Component
Here is a common pattern where DOM traversal shines — an accordion (FAQ section) where clicking a question reveals its answer:
/* HTML structure */
// <div class="accordion">
// <div class="accordion-item">
// <button class="accordion-header">Question 1</button>
// <div class="accordion-body">Answer 1</div>
// </div>
// <div class="accordion-item">
// <button class="accordion-header">Question 2</button>
// <div class="accordion-body">Answer 2</div>
// </div>
// </div>
const accordion = document.querySelector('.accordion');
accordion.addEventListener('click', (event) => {
// Use closest() to find the header that was clicked
const header = event.target.closest('.accordion-header');
if (!header) return; // Click wasn't on a header
// Traverse to the answer (next sibling of the button)
const body = header.nextElementSibling;
// Toggle visibility
body.classList.toggle('is-open');
// Close all other accordion items
const allBodies = accordion.querySelectorAll('.accordion-body');
allBodies.forEach(b => {
if (b !== body) b.classList.remove('is-open');
});
});Notice how we used closest() to go up from the click target, nextElementSibling to go sideways to the answer panel, and querySelectorAll to find all siblings. This is DOM traversal in action.
⏸️ Check Your Understanding: Combining Methods
Before moving forward, can you answer these?
- In an event handler, how can you find the nearest parent with a specific class?
- When should you use DOM traversal instead of querySelector?
Check Your Answers
- Use event.target.closest(".class-name"). closest() walks up the tree and returns the first ancestor (or the element itself) that matches the CSS selector. It returns null if no match is found.
- Use traversal when you already have a reference to a nearby element and the relationship is structural (parent, sibling, child). Use querySelector when you need to find an element by ID, class, or selector from scratch.
How confident are you with this concept?
😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!
Guided Practice: Navigate a Product Page
Traverse a Product Card Layout
You have a product listing page. Use DOM traversal to update elements without querying from document each time.
Create the product listing page
Create a new file called product-listing.html with this structure:
Product Listing
Our Products
Widget A
$19.99
Widget B
$29.99
Widget C
$39.99
Click a Details button
💡 Need a hint?
Use closest() to find the clicked card
Create app.js. Add a single click listener on the .products container (event delegation). When a button is clicked, use closest() to find the parent card:
const products = document.querySelector('.products');
const infoPanel = document.querySelector('.info-panel');
products.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const card = button.closest('.product-card');
console.log('Clicked card:', card);
});💡 Need a hint?
Traverse to get the product title and price
Inside the click handler, use traversal to get the product name and price from the card:
// The card's first child element is the h3
const title = card.firstElementChild.textContent;
// The price is the next sibling of the h3
const price = card.firstElementChild.nextElementSibling.textContent;
infoPanel.textContent = `Selected: ${title} — ${price}`;No querySelector needed — we walked from the card to its children using traversal.
💡 Need a hint?
Highlight sibling cards
When a card is clicked, add a highlight class to all its sibling cards (but not itself):
// Clear previous highlights
Array.from(card.parentElement.children).forEach(sibling => {
sibling.classList.remove('highlight');
});
// Highlight the clicked card
card.classList.add('highlight');We went up to the parent (.products), then down to all its children, then decided which ones to highlight.
💡 Need a hint?
Verify the traversal chain in DevTools
Open DevTools and add a breakpoint inside the click handler. Click a Details button and inspect each traversal step:
event.target— the buttonbutton.closest('.product-card')— the cardcard.firstElementChild— the h3card.parentElement.children— all three cards
Confirm that no document.querySelector calls were used in the traversal logic.
💡 Need a hint?
You're on track if you can:
- ☐ You can reach the product title from a button click using traversal
- ☐ You can highlight all sibling products of a clicked product
- ☐ You use closest() to find a parent container from a nested click target
- ☐ You understand when to use traversal vs querySelector
Independent Challenge: DOM Navigator
💪 DOM Navigator: Build an Interactive Tree Explorer
Now try this on your own without hints!
Your Task:
document.querySelector calls allowed for the highlighting logic.Requirements:
- Create an HTML page with at least 3 levels of nesting (e.g. body → main → section → p)
- When any element is clicked, display its tagName, parentElement, number of children, and number of siblings in an info panel
- Highlight the clicked element’s parent with a red outline
- Highlight all children with a green outline
- Highlight all siblings (excluding the clicked element) with a blue outline
- Use only DOM traversal properties (parentElement, children, nextElementSibling, etc.) — no querySelector in the highlight logic
- Clear previous highlights when a new element is clicked
Stretch Goals (Optional):
- Add a breadcrumb trail showing the path from <html> down to the clicked element using a while loop with parentElement
- Let the user navigate with keyboard arrows: Up = parentElement, Down = firstElementChild, Left = previousElementSibling, Right = nextElementSibling
- Show the difference between childNodes and children count for each clicked element
Success Criteria:
| Criteria | You've succeeded if... |
|---|---|
| Clicking highlights parent, children, and siblings correctly | Meets expectations |
| Info panel shows accurate DOM relationship data | Meets expectations |
| Only traversal properties used in highlight logic (no querySelector) | Meets expectations |
| Keyboard navigation or breadcrumb trail implemented | Exceeds expectations |
| Clean code with clear variable names and comments | Exceeds expectations |
Summary
🏁 Lesson Complete: You Can Navigate the DOM
Key Takeaways:
- The DOM is a tree of nodes with parent, child, and sibling relationships
- Use parentElement to go up, children to go down, and nextElementSibling / previousElementSibling to go sideways
- Always use the Element versions of traversal properties to skip invisible whitespace text nodes
- closest() is the most powerful traversal method — it jumps straight to the nearest matching ancestor
- DOM traversal lets you write shorter, faster code when you already have a reference to a nearby element
Learning Objectives Review:
Look back at what you set out to learn. Can you now:
- ✅ Explain the parent–child–sibling relationships in the DOM tree Check!
- ✅ Use parentNode and parentElement to move up the tree Got it!
- ✅ Use children, firstElementChild, and lastElementChild to move down Can explain it!
- ✅ Use nextElementSibling and previousElementSibling to move sideways Could teach this!
- ✅ Explain the difference between Node properties and Element properties Check!
- ✅ Combine traversal methods to reach any element from any starting point Got it!
- ✅ Apply DOM traversal in a real-world scenario Can explain it!
If you can confidently answer "yes" to most of these, you're ready to move on!
Think & Reflect:
💭 💭 Reflection Questions
- Why do you think the DOM has both Node and Element versions of traversal properties?
- In what situations would traversal be more readable than querySelector?
- How does closest() simplify event handling compared to chaining parentElement?
- Can you think of a UI pattern in a website you use daily where DOM traversal would be used behind the scenes?
🤔 Real-World Test:
DOM traversal is everywhere in professional front-end development. UI component libraries like accordions, tab panels, dropdown menus, and data tables all rely heavily on traversal to connect related elements. When a user clicks a tab header, the code uses nextElementSibling to find the panel to show. When a row in a table is deleted, the code uses parentElement to find the table body and children to recount the rows.
Modern frameworks like Vue and React abstract some of this away, but understanding traversal helps you debug layout issues, write custom components, and work with third-party libraries that manipulate the DOM directly.
🎯 Looking Ahead:
Now that you can navigate the DOM tree, you’re ready to learn how to listen for user interactions. In the Event Handling tutorial, you’ll learn about click events, keyboard events, and event delegation — and you’ll see how traversal and events work together to build interactive web pages.
Recommended Next Steps
Continue Learning
Ready to move forward? Continue with the next tutorial in this series:
Dynamic ContentRelated Topics
Explore these related tutorials to expand your knowledge:
Additional Resources
Deepen your understanding with these helpful resources:
- MDN: Traversing the DOM - Guide to navigating DOM node relationships