DOM Traversal Get Certified
Once you can select an element, the next step is often to move relative to it — up to a parent, down to children, or sideways to siblings. This is called DOM traversal.
In this lesson you will learn the most useful traversal
properties and methods: parentElement,
children, firstElementChild,
nextElementSibling, closest(),
contains(), and scoped querySelector.
Moving Up With parentElement
parentElement returns the element that directly
contains the current one, or null if there is none.
What this code does: Every element (except the root) has a
parentElement. Walking up is how you leave a nested
structure.
function makeElement(tag) {
return {
tag: tag,
textContent: "",
parentElement: null
};
}
const article = makeElement("article");
const paragraph = makeElement("p");
paragraph.textContent = "Hello";
paragraph.parentElement = article;
console.log(paragraph.parentElement.tag); // "article"
console.log(article.parentElement); // null
makeElement returns a plain object that mimics a DOM element with a tag, textContent, and a parentElement link.const article = makeElement("article");paragraph.parentElement = article; establishes the parent relationship that real DOM elements expose.console.log(paragraph.parentElement.tag); prints "article".console.log(article.parentElement); prints null — the same behaviour as a real root element.Moving Down With children
children is a live collection of an element’s
direct element children (no text nodes).
What this code does: children lists only element
children. Useful when you need every direct child of a container.
function makeElement(tag) {
return {
tag: tag,
textContent: "",
children: []
};
}
const list = makeElement("ul");
const item1 = makeElement("li");
item1.textContent = "Apples";
const item2 = makeElement("li");
item2.textContent = "Bananas";
list.children.push(item1, item2);
console.log(list.children.length); // 2
console.log(list.children[0].textContent); // "Apples"
children: [] will hold the child elements, just like the real DOM children collection.ul and two li elements with text.list.children.push(item1, item2); adds both items to the parent’s children list.console.log(list.children.length); prints 2.console.log(list.children[0].textContent); prints "Apples".firstElementChild and lastElementChild
What this code does: Shortcuts to the first and last element children of a parent.
function makeElement(tag) {
return { tag: tag, textContent: "", children: [] };
}
const nav = makeElement("nav");
const home = makeElement("a");
home.textContent = "Home";
const about = makeElement("a");
about.textContent = "About";
const contact = makeElement("a");
contact.textContent = "Contact";
nav.children.push(home, about, contact);
console.log(nav.children[0].textContent); // first
console.log(nav.children[nav.children.length - 1].textContent); // last
children array.Home, About, and Contact anchors and attaches them.nav.children[0] is the equivalent of firstElementChild.nav.children[nav.children.length - 1] is the equivalent of lastElementChild.Moving Sideways With Siblings
nextElementSibling and
previousElementSibling let you step from one element
to the one right beside it.
What this code does: Sibling links connect elements that share a parent.
nextElementSibling and previousElementSibling move sideways.
function makeElement(tag) {
return {
tag: tag,
textContent: "",
parentElement: null,
nextElementSibling: null,
previousElementSibling: null
};
}
function appendChild(parent, child) {
child.parentElement = parent;
const lastChild = parent.children[parent.children.length - 1];
if (lastChild) {
lastChild.nextElementSibling = child;
child.previousElementSibling = lastChild;
}
parent.children.push(child);
}
const tabBar = { children: [] };
const tabOne = makeElement("button");
tabOne.textContent = "Overview";
const tabTwo = makeElement("button");
tabTwo.textContent = "Specifications";
const tabThree = makeElement("button");
tabThree.textContent = "Reviews";
appendChild(tabBar, tabOne);
appendChild(tabBar, tabTwo);
appendChild(tabBar, tabThree);
console.log(tabTwo.previousElementSibling.textContent);
console.log(tabTwo.nextElementSibling.textContent);
nextElementSibling and previousElementSibling are initialised to null.nextElementSibling and the new child’s previousElementSibling, then pushes the child onto the parent.tabTwo.previousElementSibling is Overview; tabTwo.nextElementSibling is Reviews.Node Properties vs. Element Properties
On a real page, properties like childNodes,
firstChild, and nextSibling also exist,
but they include every type of node — including
whitespace text nodes created by line breaks and indentation in
your HTML. The Element versions
(children, firstElementChild,
nextElementSibling) skip those text nodes and only
include actual elements, which is usually what you want.
| Node Version (includes text nodes) | Element Version (elements only) |
|---|---|
| childNodes | children |
| firstChild | firstElementChild |
| lastChild | lastElementChild |
| nextSibling | nextElementSibling |
| previousSibling | previousElementSibling |
In most everyday code, the Element versions are the ones you’ll want, since dealing with unexpected whitespace text nodes is rarely useful.
Searching Within a Specific Element
querySelector() and querySelectorAll()
aren’t limited to the whole document — calling them on a specific
element searches only within that element’s descendants.
What this code does: Scoped querySelectorAll searches only
inside a container, not the whole document.
function makeElement(tag, className) {
return {
tag: tag,
className: className || "",
children: [],
appendChild(child) { this.children.push(child); },
querySelectorAll(selector) {
const matches = [];
function search(element) {
element.children.forEach(function (child) {
if ("." + child.className === selector) {
matches.push(child);
}
search(child);
});
}
search(this);
return matches;
}
};
}
const orderSummary = makeElement("section");
const lineItemsContainer = makeElement("div");
orderSummary.appendChild(lineItemsContainer);
["Keyboard", "Mouse", "Monitor"].forEach(function (itemName) {
const lineItem = makeElement("div", "line-item");
lineItem.name = itemName;
lineItemsContainer.appendChild(lineItem);
});
const lineItems = orderSummary.querySelectorAll(".line-item");
console.log(lineItems.map(item => item.name));
querySelectorAll walks the subtree and collects every child whose class matches the given selector (simplified to class-only matching).line-item and a name.orderSummary.querySelectorAll(".line-item") finds the three items without searching the whole document, then their names are logged.Finding an Ancestor With closest()
closest() searches upward from an element, through
its parents, and returns the nearest ancestor (or the element
itself) that matches a given selector. It’s especially useful
inside click handlers, to find a containing element from wherever
inside it was clicked.
What this code does: closest() walks up the tree until it
finds an ancestor that matches a selector (or returns null).
function makeElement(tag, className) {
return {
tag: tag,
className: className || "",
parentElement: null,
closest(selector) {
let current = this;
while (current) {
if ("." + current.className === selector) {
return current;
}
current = current.parentElement;
}
return null;
}
};
}
function appendChild(parent, child) {
child.parentElement = parent;
}
const productCard = makeElement("article", "product-card");
const priceTag = makeElement("span", "price-tag");
appendChild(productCard, priceTag);
const foundCard = priceTag.closest(".product-card");
console.log(foundCard === productCard);
closest walks upward via parentElement until it finds a matching class or reaches the top (null).parentElement so the upward walk works.priceTag.closest(".product-card") returns the same object as productCard, demonstrating the upward search.Checking Containment With contains()
contains() checks whether one element is a
descendant of another (or is the element itself), which is useful
for things like detecting clicks outside of a dropdown menu.
What this code does: contains() checks whether one element
is nested inside another — useful for “click outside” logic.
function makeElement(tag) {
return {
tag: tag,
children: [],
appendChild(child) { this.children.push(child); },
contains(target) {
if (this === target) return true;
return this.children.some(function (child) {
return child.contains ? child.contains(target) : child === target;
});
}
};
}
const dropdownMenu = makeElement("div");
const menuOption = makeElement("li");
dropdownMenu.appendChild(menuOption);
const unrelatedButton = makeElement("button");
console.log(dropdownMenu.contains(menuOption));
console.log(dropdownMenu.contains(unrelatedButton));
contains returns true for itself or for any descendant found by a recursive search of children.dropdownMenu.contains(menuOption) prints true.dropdownMenu.contains(unrelatedButton) prints false.Practical Example
Let’s use traversal to highlight a single selected tab and reset its siblings, a common pattern in tabbed interfaces.
What this code does: Builds a tab bar and demonstrates selecting a tab and relating it to its neighbors in the same parent.
function makeElement(tag) {
return {
tag: tag,
textContent: "",
parentElement: null,
selected: false
};
}
function buildTabBar(labels) {
const tabBar = { children: [] };
labels.forEach(function (label) {
const tab = makeElement("button");
tab.textContent = label;
tab.parentElement = tabBar;
tabBar.children.push(tab);
});
return tabBar;
}
function selectTab(tabBar, tabToSelect) {
tabBar.children.forEach(function (tab) {
tab.selected = (tab === tabToSelect);
});
}
const tabBar = buildTabBar(["Overview", "Specifications", "Reviews"]);
selectTab(tabBar, tabBar.children[1]);
tabBar.children.forEach(function (tab) {
console.log(tab.textContent, "-> selected:", tab.selected);
});
selected flag and a parent link.selected to true only for the chosen tab and false for its siblings — the classic “one active at a time” pattern.Common Mistakes With DOM Traversal
- Using
childNodesorfirstChildwhenchildrenorfirstElementChildwas intended, and being surprised by unexpected text nodes. - Forgetting that
parentElementisnullfor the topmost element, and not checking for that before using it further. - Assuming
nextElementSiblingwill always return something, when it returnsnullfor the last element in a group. - Using
closest()with a selector that doesn’t match anything in the element’s ancestor chain, and not handling the resultingnull. - Calling
querySelector()on the whole document when searching within a specific container would be faster and more precise.
Best Practices for DOM Traversal
- Prefer the Element-specific properties (
children,firstElementChild,nextElementSibling) over their Node counterparts in most everyday code. - Always check for
nullbefore continuing to traverse, since reaching the edge of the tree is common. - Use
closest()inside event handlers to reliably find a containing element, regardless of exactly where inside it was clicked. - Scope
querySelector()calls to the smallest relevant container rather than always searching the entire document. - Use
contains()for reliable “is this inside that?” checks instead of manually walking up the tree yourself.
Quick Summary
parentElementmoves up to a containing element.children,firstElementChild, andlastElementChildmove down to child elements.nextElementSiblingandpreviousElementSiblingmove sideways between elements that share a parent.- Element-specific properties skip whitespace text nodes; their Node counterparts do not.
closest()searches upward for a matching ancestor;contains()checks whether one element is nested inside another.querySelector()andquerySelectorAll()can be scoped to search within a specific element instead of the whole page.
Try It Yourself
Modify the code below. Try selecting a different tab, or logging each tab’s neighbors using the sibling links.
What this code does: Practice example: a short step list linked by siblings. Try changing the steps or logging more neighbors, then click Test My Code.
function makeElement(tag) {
return {
tag: tag,
textContent: "",
nextElementSibling: null,
previousElementSibling: null
};
}
const step1 = makeElement("li");
step1.textContent = "Sign up";
const step2 = makeElement("li");
step2.textContent = "Verify email";
const step3 = makeElement("li");
step3.textContent = "Complete profile";
step1.nextElementSibling = step2;
step2.previousElementSibling = step1;
step2.nextElementSibling = step3;
step3.previousElementSibling = step2;
console.log("Step after 'Sign up':", step1.nextElementSibling.textContent);
console.log("Step before 'Complete profile':", step3.previousElementSibling.textContent);
step1.nextElementSibling is “Verify email”; step3.previousElementSibling is also “Verify email”. Try changing the links or logging more neighbours.Tip: When working with click events, reach forclosest()instead of manually checkingevent.targetand its parents one by one — it’s built exactly for that “find the containing thing” problem, and it’s far less error-prone.
Understanding DOM traversal is essential before moving on to event delegation, forms, and more advanced browser-based JavaScript concepts.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.