DOM Traversal
DOM traversal is navigating the document tree to find elements relative to other elements. Instead of searching the entire document, traverse from known elements to their relatives.
The DOM Tree Structure
HTML creates a tree of nodes:
<div id="container">
<h2>Title</h2>
<p>First paragraph</p>
<p>Second paragraph</p>
</div>
container (div)
├── h2 (Title)
├── p (First paragraph)
└── p (Second paragraph)
Parent Elements
parentElement
Get the parent element:
const paragraph = document.querySelector('p');
// Get parent
const parent = paragraph.parentElement;
console.log(parent.tagName); // "DIV"
// Chain to go up multiple levels
const grandparent = paragraph.parentElement.parentElement;
parentNode vs parentElement
const element = document.querySelector('#child');
// Usually the same
console.log(element.parentElement); // <div>
console.log(element.parentNode); // <div>
// Difference: document has no parentElement
console.log(document.documentElement.parentElement); // null
console.log(document.documentElement.parentNode); // #document
Use parentElement - more intuitive for regular elements.
closest()
Find the nearest ancestor matching a selector:
const button = document.querySelector('button');
// Find closest form
const form = button.closest('form');
// Find closest parent with class
const card = button.closest('.card');
// Find closest with data attribute
const container = button.closest('[data-container]');
// Returns null if not found
const modal = button.closest('.modal'); // null if not in modal
Very useful for event delegation:
document.addEventListener('click', (e) => {
// Check if click was on a delete button
const deleteButton = e.target.closest('.delete-button');
if (deleteButton) {
// Find which card the button belongs to
const card = deleteButton.closest('.card');
const cardId = card.dataset.id;
deleteCard(cardId);
}
});
Child Elements
children
Get all child elements (not text nodes):
const container = document.querySelector('#container');
// Get all children
const children = container.children; // HTMLCollection
console.log(children.length); // 3
// Access by index
const firstChild = container.children[0];
const secondChild = container.children[1];
// Convert to array for iteration
Array.from(children).forEach(child => {
console.log(child.tagName);
});
firstElementChild / lastElementChild
const list = document.querySelector('ul');
// Get first and last child elements
const first = list.firstElementChild;
const last = list.lastElementChild;
console.log(first.textContent); // First item
console.log(last.textContent); // Last item
childNodes (includes text nodes)
const div = document.querySelector('div');
// Get all nodes (including text, comments)
const nodes = div.childNodes;
// Usually you want children instead
const elements = div.children;
Prefer children over childNodes for elements.
childElementCount
const list = document.querySelector('ul');
// Count child elements
const count = list.childElementCount;
console.log(`List has ${count} items`);
// Same as
console.log(list.children.length);
Sibling Elements
nextElementSibling
Get the next sibling element:
const paragraph = document.querySelector('p');
// Get next paragraph
const next = paragraph.nextElementSibling;
// Chain to skip elements
const afterNext = paragraph.nextElementSibling.nextElementSibling;
// Returns null if no next sibling
const last = document.querySelector('p:last-child');
console.log(last.nextElementSibling); // null
previousElementSibling
Get the previous sibling:
const paragraph = document.querySelector('p:nth-child(3)');
// Get previous paragraph
const previous = paragraph.previousElementSibling;
// Returns null if no previous sibling
const first = document.querySelector('p:first-child');
console.log(first.previousElementSibling); // null
Practical Sibling Navigation
// Highlight current and adjacent items
const currentItem = document.querySelector('.current');
if (currentItem.previousElementSibling) {
currentItem.previousElementSibling.classList.add('before-current');
}
if (currentItem.nextElementSibling) {
currentItem.nextElementSibling.classList.add('after-current');
}
// Get all siblings
function getAllSiblings(element) {
const siblings = [];
let sibling = element.parentElement.firstElementChild;
while (sibling) {
if (sibling !== element) {
siblings.push(sibling);
}
sibling = sibling.nextElementSibling;
}
return siblings;
}
Traversal Methods Comparison
<div id="container">
<h2>Title</h2>
<p id="first">Paragraph 1</p>
<p id="second">Paragraph 2</p>
<p id="third">Paragraph 3</p>
</div>
const second = document.querySelector('#second');
// Parent
second.parentElement; // <div id="container">
second.closest('#container'); // <div id="container">
// Children (from container)
const container = document.querySelector('#container');
container.children; // [h2, p, p, p]
container.firstElementChild; // <h2>
container.lastElementChild; // <p id="third">
container.children[1]; // <p id="first">
// Siblings
second.previousElementSibling; // <p id="first">
second.nextElementSibling; // <p id="third">
Practical Examples
Tab Navigation
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Hide all tab contents
const tabContainer = tab.closest('.tab-container');
const allContents = tabContainer.querySelectorAll('.tab-content');
allContents.forEach(content => content.classList.add('hidden'));
// Remove active class from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Show clicked tab content
tab.classList.add('active');
const tabId = tab.dataset.tab;
const content = document.querySelector(`#${tabId}`);
content.classList.remove('hidden');
});
});
Accordion
const accordionHeaders = document.querySelectorAll('.accordion-header');
accordionHeaders.forEach(header => {
header.addEventListener('click', () => {
// Get the content panel (next sibling)
const content = header.nextElementSibling;
// Toggle content
content.classList.toggle('open');
// Toggle header active state
header.classList.toggle('active');
});
});
Nested List Navigation
const nestedList = document.querySelector('#nested-list');
nestedList.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
const li = e.target;
// Find nested ul (child)
const nestedUl = li.querySelector('ul');
if (nestedUl) {
nestedUl.classList.toggle('expanded');
}
// Prevent event from bubbling to parent li
e.stopPropagation();
}
});
Breadcrumb Trail
function createBreadcrumb(element) {
const breadcrumb = [];
let current = element;
// Traverse up to body
while (current && current !== document.body) {
breadcrumb.unshift({
tag: current.tagName,
id: current.id,
classes: Array.from(current.classList)
});
current = current.parentElement;
}
return breadcrumb;
}
const element = document.querySelector('#target');
console.log(createBreadcrumb(element));
// [
// { tag: 'HTML', id: '', classes: [] },
// { tag: 'BODY', id: '', classes: [] },
// { tag: 'DIV', id: 'container', classes: ['wrapper'] },
// { tag: 'P', id: 'target', classes: ['text'] }
// ]
Form Field Dependencies
const countrySelect = document.querySelector('#country');
countrySelect.addEventListener('change', () => {
// Find the form
const form = countrySelect.closest('form');
// Find state field (next input after country)
const stateField = countrySelect
.parentElement
.nextElementSibling
.querySelector('select');
if (countrySelect.value === 'US') {
stateField.disabled = false;
stateField.required = true;
} else {
stateField.disabled = true;
stateField.required = false;
}
});
Table Row Actions
const table = document.querySelector('#data-table');
table.addEventListener('click', (e) => {
// Find if click was on delete button
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
// Get the row
const row = deleteBtn.closest('tr');
// Get data from cells
const cells = row.children;
const id = cells[0].textContent;
const name = cells[1].textContent;
if (confirm(`Delete ${name}?`)) {
row.remove();
console.log(`Deleted row ${id}`);
}
}
});
Slideshow Navigation
const slides = document.querySelectorAll('.slide');
let currentSlide = document.querySelector('.slide.active');
function nextSlide() {
const next = currentSlide.nextElementSibling;
if (next && next.classList.contains('slide')) {
currentSlide.classList.remove('active');
next.classList.add('active');
currentSlide = next;
} else {
// Wrap to first slide
currentSlide.classList.remove('active');
slides[0].classList.add('active');
currentSlide = slides[0];
}
}
function previousSlide() {
const prev = currentSlide.previousElementSibling;
if (prev && prev.classList.contains('slide')) {
currentSlide.classList.remove('active');
prev.classList.add('active');
currentSlide = prev;
} else {
// Wrap to last slide
currentSlide.classList.remove('active');
const lastSlide = slides[slides.length - 1];
lastSlide.classList.add('active');
currentSlide = lastSlide;
}
}
Combining querySelector with Traversal
Often combine both approaches:
const button = document.querySelector('#save-button');
// Traverse to find form
const form = button.closest('form');
// Query within form
const emailInput = form.querySelector('input[type="email"]');
const allInputs = form.querySelectorAll('input');
// Traverse from input
const label = emailInput.previousElementSibling;
const errorMsg = emailInput.nextElementSibling;
Performance Considerations
// ✅ GOOD: Store reference
const container = document.querySelector('#container');
const children = container.children;
for (let i = 0; i < children.length; i++) {
children[i].classList.add('styled');
}
// ❌ SLOW: Query every iteration
for (let i = 0; i < 100; i++) {
const item = document.querySelector(`#item-${i}`);
item.style.color = 'red';
}
// ✅ BETTER: Traverse from parent
const list = document.querySelector('#list');
Array.from(list.children).forEach(item => {
item.style.color = 'red';
});
Best Practices
✅ DO:
- Use
parentElement,children,nextElementSiblingfor elements - Use
closest()for finding ancestor with selector - Store element references to avoid repeated queries
- Use traversal within a known subtree instead of querying whole document
❌ DON’T:
- Use
parentNodewhen you wantparentElement - Use
childNodeswhen you wantchildren - Query the entire document when you can traverse locally
- Forget to check for
nullwhen traversing
Summary
| Direction | Property | Method |
|---|---|---|
| Parent | parentElement |
Get direct parent |
closest(selector) |
Find ancestor matching selector | |
| Children | children |
Get all child elements |
firstElementChild |
Get first child | |
lastElementChild |
Get last child | |
| Siblings | nextElementSibling |
Get next sibling |
previousElementSibling |
Get previous sibling |
Common patterns:
element.parentElement // Go up one level
element.closest('.card') // Find ancestor
element.children[0] // First child
element.nextElementSibling // Next sibling
element.querySelector('selector') // Search down
Next Article: Event Handling Advanced