DOM Traversal: Parent, Child, and Sibling
What Is DOM Traversal?
DOM traversal is the process of navigating between nodes in the Document Object Model tree. Every HTML document is represented as a tree of nodes, and understanding how to move between those nodes is fundamental to writing effective JavaScript. Instead of always using querySelector or getElementById to find elements from scratch, traversal lets you start at one node and walk to related nodes -- parents, children, and siblings. This is faster, more efficient, and often essential when you are working with dynamically generated content where IDs and classes may not be known in advance.
Think of the DOM like a family tree. Every element has a parent (the element that contains it), may have children (elements nested inside it), and may have siblings (elements at the same level). Mastering traversal means you can reach any node in the document starting from any other node, which gives you complete control over page manipulation.
Parent Traversal: parentNode vs parentElement
When you need to go up the DOM tree from a given node, you have two properties at your disposal: parentNode and parentElement. They may seem identical, but there is an important difference.
parentNode returns the parent of any node type, including text nodes, comment nodes, and document nodes. parentElement returns the parent only if that parent is an element node. For most everyday use cases, both return the same result because elements are typically nested inside other elements. The difference only matters at the very top of the tree: the parent of the <html> element is the document node, which is not an element node. So document.documentElement.parentNode returns the document, but document.documentElement.parentElement returns null.
Example: parentNode vs parentElement
<div id="container">
<p id="message">Hello World</p>
</div>
<script>
const message = document.getElementById('message');
// Both return the div#container
console.log(message.parentNode); // <div id="container">
console.log(message.parentElement); // <div id="container">
// At the top of the tree, they differ
const html = document.documentElement;
console.log(html.parentNode); // #document
console.log(html.parentElement); // null
</script>
parentElement is the safer choice because you almost always want to work with element nodes. Use parentNode only when you specifically need to handle non-element nodes like the document node itself.Climbing Multiple Levels
You can chain parent properties to climb multiple levels up the DOM tree. This is useful when you need to reach a grandparent or even higher ancestor from a deeply nested element.
Example: Climbing Multiple Levels
<section id="main">
<div class="card">
<p class="card-text">
<span id="highlight">Important</span>
</p>
</div>
</section>
<script>
const highlight = document.getElementById('highlight');
// Go up one level: <p class="card-text">
console.log(highlight.parentElement);
// Go up two levels: <div class="card">
console.log(highlight.parentElement.parentElement);
// Go up three levels: <section id="main">
console.log(highlight.parentElement.parentElement.parentElement);
</script>
.parentElement calls makes code fragile. If the HTML structure changes and a wrapper is added or removed, your chain breaks. Use closest() instead for reliable ancestor lookups -- we will cover that later in this lesson.Child Traversal: childNodes vs children
When you want to go down the tree and access the nodes nested inside an element, you have two properties: childNodes and children. Understanding the difference between them is critical because they return very different collections.
childNodes returns a live NodeList containing every child node, including text nodes (whitespace, line breaks), comment nodes, and element nodes. children returns a live HTMLCollection containing only child element nodes. In most cases, you want children because whitespace text nodes are rarely useful.
Example: childNodes vs children
<ul id="menu">
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
<script>
const menu = document.getElementById('menu');
// childNodes includes text nodes (whitespace between tags)
console.log(menu.childNodes); // NodeList(7) [text, li, text, li, text, li, text]
console.log(menu.childNodes.length); // 7
// children includes only element nodes
console.log(menu.children); // HTMLCollection(3) [li, li, li]
console.log(menu.children.length); // 3
</script>
childNodes returns 7 nodes for 3 list items -- there are 4 text nodes containing whitespace (before the first li, between each li, and after the last li). This is one of the most common sources of confusion when working with the DOM.First and Last Child
To quickly access the first or last child of an element, you have two pairs of properties. The firstChild and lastChild properties include all node types. The firstElementChild and lastElementChild properties include only element nodes.
Example: First and Last Child Properties
<div id="wrapper">
<h2>Title</h2>
<p>First paragraph</p>
<p>Second paragraph</p>
</div>
<script>
const wrapper = document.getElementById('wrapper');
// firstChild is a text node (whitespace before <h2>)
console.log(wrapper.firstChild); // #text
console.log(wrapper.firstChild.nodeType); // 3 (Text node)
// firstElementChild is the <h2>
console.log(wrapper.firstElementChild); // <h2>Title</h2>
console.log(wrapper.firstElementChild.nodeType); // 1 (Element node)
// lastChild is a text node (whitespace after last <p>)
console.log(wrapper.lastChild); // #text
// lastElementChild is the second <p>
console.log(wrapper.lastElementChild); // <p>Second paragraph</p>
</script>
Element versions (firstElementChild, lastElementChild) unless you have a specific reason to work with text or comment nodes. This prevents unexpected behavior caused by whitespace text nodes.Sibling Traversal: Moving Sideways in the DOM
Siblings are nodes that share the same parent. Just like parent and child properties, sibling properties come in two flavors: ones that include all node types and ones that include only element nodes.
nextSibling and previousSibling return the next or previous node of any type. nextElementSibling and previousElementSibling return only the next or previous element node. Again, the element-only versions are what you typically want.
Example: Sibling Traversal
<ul id="nav">
<li id="home">Home</li>
<li id="about">About</li>
<li id="services">Services</li>
<li id="contact">Contact</li>
</ul>
<script>
const about = document.getElementById('about');
// nextSibling is a text node (whitespace)
console.log(about.nextSibling); // #text
// nextElementSibling is the <li id="services">
console.log(about.nextElementSibling); // <li id="services">
// previousSibling is a text node (whitespace)
console.log(about.previousSibling); // #text
// previousElementSibling is the <li id="home">
console.log(about.previousElementSibling); // <li id="home">
// First element has no previous sibling
const home = document.getElementById('home');
console.log(home.previousElementSibling); // null
// Last element has no next sibling
const contact = document.getElementById('contact');
console.log(contact.nextElementSibling); // null
</script>
The closest() Method
The closest() method is one of the most powerful traversal tools available. It walks up the DOM tree from the current element and returns the first ancestor (or the element itself) that matches a given CSS selector. If no matching ancestor is found, it returns null. This is far more robust than chaining parentElement because it does not depend on the exact number of levels between elements.
Example: Using closest()
<div class="card" data-id="42">
<div class="card-body">
<h3 class="card-title">Product Name</h3>
<p class="card-text">Description here</p>
<button class="btn-delete">Delete</button>
</div>
</div>
<script>
const deleteBtn = document.querySelector('.btn-delete');
// Find the closest ancestor with class "card"
const card = deleteBtn.closest('.card');
console.log(card); // <div class="card" data-id="42">
console.log(card.dataset.id); // "42"
// Find the closest ancestor with class "card-body"
const body = deleteBtn.closest('.card-body');
console.log(body); // <div class="card-body">
// No match returns null
const form = deleteBtn.closest('form');
console.log(form); // null
</script>
closest() method checks the element itself first, then walks up through its ancestors. So if the element itself matches the selector, it returns itself. This is different from parentElement which always starts from the parent.The contains() Method
The contains() method checks whether a node is a descendant of another node. It returns true if the argument node is a child, grandchild, or any deeper descendant of the node you call it on. It also returns true if you pass the node itself. This is extremely useful for checking whether a click happened inside or outside a particular element.
Example: Using contains()
<div id="dropdown">
<button id="toggle">Menu</button>
<ul id="menu-list">
<li>Option 1</li>
<li>Option 2</li>
</ul>
</div>
<script>
const dropdown = document.getElementById('dropdown');
const toggle = document.getElementById('toggle');
const menuList = document.getElementById('menu-list');
// Check if toggle is inside dropdown
console.log(dropdown.contains(toggle)); // true
// Check if dropdown contains itself
console.log(dropdown.contains(dropdown)); // true
// Check if toggle contains dropdown (reverse)
console.log(toggle.contains(dropdown)); // false
// Real-world use: close dropdown when clicking outside
document.addEventListener('click', function(event) {
if (!dropdown.contains(event.target)) {
menuList.style.display = 'none';
}
});
</script>
Walking the DOM Tree Recursively
Sometimes you need to visit every node in a subtree -- for example, to find all text content, highlight certain elements, or count specific node types. You can write a recursive function that visits an element and then calls itself on each of that element's children. This technique is called walking the DOM tree.
Example: Recursive DOM Walker
<div id="content">
<h2>Title</h2>
<div>
<p>Paragraph <strong>with bold</strong> text</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
</div>
<script>
function walkDOM(node, callback) {
// Call the callback on the current node
callback(node);
// Get the first child
let child = node.firstElementChild;
// Visit each child recursively
while (child) {
walkDOM(child, callback);
child = child.nextElementSibling;
}
}
// Example: log every element tag name
const content = document.getElementById('content');
walkDOM(content, function(element) {
console.log(element.tagName);
});
// Output: DIV, H2, DIV, P, STRONG, UL, LI, LI
</script>
Iterating Over NodeLists and HTMLCollections
When you use properties like childNodes or methods like querySelectorAll, you get a NodeList. When you use children, you get an HTMLCollection. Both are array-like objects but they are not actual arrays. Knowing how to iterate over them is essential for DOM traversal.
Example: Iterating NodeList and HTMLCollection
<ul id="list">
<li class="item">Apple</li>
<li class="item">Banana</li>
<li class="item">Cherry</li>
</ul>
<script>
const list = document.getElementById('list');
// Method 1: for loop (works with both NodeList and HTMLCollection)
const children = list.children;
for (let i = 0; i < children.length; i++) {
console.log(children[i].textContent);
}
// Method 2: for...of loop (works with NodeList from querySelectorAll)
const items = document.querySelectorAll('.item');
for (const item of items) {
console.log(item.textContent);
}
// Method 3: forEach (works with NodeList from querySelectorAll)
items.forEach(function(item, index) {
console.log(index + ': ' + item.textContent);
});
// Method 4: Convert HTMLCollection to array for array methods
const childArray = Array.from(list.children);
childArray.forEach(function(child) {
console.log(child.textContent);
});
// Method 5: Spread operator to convert to array
const spreadArray = [...list.children];
spreadArray.map(function(child) {
return child.textContent.toUpperCase();
});
</script>
.forEach() directly on an HTMLCollection (from .children) will throw an error because HTMLCollection does not have a forEach method. Always convert it to an array first with Array.from() or the spread operator, or use a standard for loop.Filtering Children by Criteria
Often you need to find specific children that match certain criteria -- a particular class, tag name, attribute, or text content. You can combine traversal with filtering to achieve this efficiently.
Example: Filtering Children
<div id="toolbar">
<button class="btn primary">Save</button>
<button class="btn">Cancel</button>
<span class="separator">|</span>
<button class="btn danger">Delete</button>
<button class="btn" disabled>Archive</button>
</div>
<script>
const toolbar = document.getElementById('toolbar');
// Filter children to only buttons
const buttons = Array.from(toolbar.children).filter(function(child) {
return child.tagName === 'BUTTON';
});
console.log(buttons.length); // 4
// Filter to only enabled buttons
const enabledButtons = Array.from(toolbar.children).filter(function(child) {
return child.tagName === 'BUTTON' && !child.disabled;
});
console.log(enabledButtons.length); // 3
// Find the first button with the "danger" class
const dangerBtn = Array.from(toolbar.children).find(function(child) {
return child.classList.contains('danger');
});
console.log(dangerBtn.textContent); // "Delete"
// Check if any child has the "primary" class
const hasPrimary = Array.from(toolbar.children).some(function(child) {
return child.classList.contains('primary');
});
console.log(hasPrimary); // true
</script>
Real-World Example: Dynamic Navigation Menu
Let us build a practical navigation menu that uses DOM traversal to handle user interactions. This example demonstrates how traversal is used in real applications to manage active states, expand submenus, and handle keyboard navigation.
Example: Interactive Navigation with DOM Traversal
<nav id="main-nav">
<ul class="nav-list">
<li class="nav-item active">
<a href="#home">Home</a>
</li>
<li class="nav-item has-submenu">
<a href="#products">Products</a>
<ul class="submenu">
<li><a href="#software">Software</a></li>
<li><a href="#hardware">Hardware</a></li>
<li><a href="#services">Services</a></li>
</ul>
</li>
<li class="nav-item">
<a href="#about">About</a>
</li>
<li class="nav-item">
<a href="#contact">Contact</a>
</li>
</ul>
</nav>
<script>
const navList = document.querySelector('.nav-list');
// Handle click on any nav link
navList.addEventListener('click', function(event) {
const link = event.target.closest('a');
if (!link) return;
event.preventDefault();
// Use closest() to find the parent nav-item
const navItem = link.closest('.nav-item');
// Use parentElement to get the nav-list level
const parentList = navItem.parentElement;
// Remove active from all siblings at the same level
Array.from(parentList.children).forEach(function(sibling) {
sibling.classList.remove('active');
});
// Set current item as active
navItem.classList.add('active');
// Toggle submenu if this item has one
if (navItem.classList.contains('has-submenu')) {
const submenu = navItem.querySelector('.submenu');
const isVisible = submenu.style.display === 'block';
submenu.style.display = isVisible ? 'none' : 'block';
}
// Close other submenus using sibling traversal
let sibling = navItem.previousElementSibling;
while (sibling) {
const sub = sibling.querySelector('.submenu');
if (sub) sub.style.display = 'none';
sibling = sibling.previousElementSibling;
}
sibling = navItem.nextElementSibling;
while (sibling) {
const sub = sibling.querySelector('.submenu');
if (sub) sub.style.display = 'none';
sibling = sibling.nextElementSibling;
}
});
</script>
Real-World Example: Table Row Highlighting
Another common pattern is traversing table rows and cells. When a user clicks a cell, you might want to highlight the entire row, find the header for that column, or navigate between rows.
Example: Table Traversal and Highlighting
<table id="data-table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice</td>
<td>alice@example.com</td>
<td>Admin</td>
</tr>
<tr>
<td>Bob</td>
<td>bob@example.com</td>
<td>Editor</td>
</tr>
<tr>
<td>Carol</td>
<td>carol@example.com</td>
<td>Viewer</td>
</tr>
</tbody>
</table>
<script>
const table = document.getElementById('data-table');
const tbody = table.querySelector('tbody');
tbody.addEventListener('click', function(event) {
const cell = event.target.closest('td');
if (!cell) return;
// Get the parent row using parentElement
const row = cell.parentElement;
// Remove highlight from all sibling rows
Array.from(tbody.children).forEach(function(tr) {
tr.style.backgroundColor = '';
});
// Highlight the clicked row
row.style.backgroundColor = '#e8f4fd';
// Get cell index to find the column header
const cellIndex = Array.from(row.children).indexOf(cell);
const headerRow = table.querySelector('thead tr');
const header = headerRow.children[cellIndex];
console.log('Column: ' + header.textContent);
console.log('Value: ' + cell.textContent);
// Navigate to the next row (if exists)
const nextRow = row.nextElementSibling;
if (nextRow) {
const sameColumnCell = nextRow.children[cellIndex];
console.log('Next row value: ' + sameColumnCell.textContent);
}
});
</script>
Real-World Example: Accordion Component
Accordions are a common UI pattern where clicking a header toggles the visibility of the content below it. DOM traversal makes this pattern clean and maintainable without needing to assign unique IDs to every panel.
Example: Accordion Using Sibling Traversal
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header">Section 1</button>
<div class="accordion-panel">
<p>Content for section 1 goes here.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">Section 2</button>
<div class="accordion-panel">
<p>Content for section 2 goes here.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">Section 3</button>
<div class="accordion-panel">
<p>Content for section 3 goes here.</p>
</div>
</div>
</div>
<script>
const accordion = document.querySelector('.accordion');
accordion.addEventListener('click', function(event) {
const header = event.target.closest('.accordion-header');
if (!header) return;
// Get the panel (next sibling element of the header)
const panel = header.nextElementSibling;
// Get the parent accordion-item
const item = header.parentElement;
// Close all other panels using parent traversal
const allItems = item.parentElement.children;
Array.from(allItems).forEach(function(otherItem) {
if (otherItem !== item) {
const otherPanel = otherItem.querySelector('.accordion-panel');
otherPanel.style.display = 'none';
otherItem.querySelector('.accordion-header').classList.remove('active');
}
});
// Toggle current panel
const isOpen = panel.style.display === 'block';
panel.style.display = isOpen ? 'none' : 'block';
header.classList.toggle('active');
});
</script>
Traversal Summary Table
Here is a reference table that summarizes all the traversal properties and methods covered in this lesson. Keep this handy as you work through DOM manipulation tasks.
Quick Reference: All Traversal Properties
// PARENT TRAVERSAL
element.parentNode // Parent node (any type)
element.parentElement // Parent element node only
// CHILD TRAVERSAL
element.childNodes // All child nodes (NodeList)
element.children // Child element nodes only (HTMLCollection)
element.firstChild // First child node (any type)
element.firstElementChild // First child element node
element.lastChild // Last child node (any type)
element.lastElementChild // Last child element node
// SIBLING TRAVERSAL
element.nextSibling // Next node (any type)
element.nextElementSibling // Next element node
element.previousSibling // Previous node (any type)
element.previousElementSibling // Previous element node
// TRAVERSAL METHODS
element.closest(selector) // Nearest ancestor matching selector
element.contains(node) // Check if node is a descendant
// USEFUL PROPERTIES
element.childElementCount // Number of child elements
element.hasChildNodes() // Returns true if has any child nodes
Practice Exercise
Create an HTML page with a nested comment thread structure -- a list of comments where each comment can have replies nested inside it. Each comment should have the author name, comment text, and a "Reply" button. Using only DOM traversal (no querySelector calls in your event handlers), write JavaScript that does the following: (1) When a "Reply" button is clicked, use closest() to find the parent comment element and parentElement to find the replies container. (2) Use children to count how many replies that comment already has and display the count. (3) Use nextElementSibling and previousElementSibling to add "Next Comment" and "Previous Comment" navigation buttons that highlight sibling comments. (4) Write a recursive walkDOM function that counts the total number of comments and replies in the entire thread. (5) Use contains() to detect clicks outside the comment thread and collapse all expanded reply sections. Test your solution thoroughly and verify that every traversal returns the expected element.