DOM Traversal
Once you've selected an element, you often need to find something near it — its parent, its siblings, a specific descendant, or the nearest ancestor that matches some condition. The DOM gives you a small toolbox for walking the tree without needing to re-query from document every time. This page covers the moves you'll actually use.
The example tree
tree.html
<section id="todo">
<h2>Today</h2>
<ul class="list">
<li class="task" data-id="1">Buy milk</li>
<li class="task done" data-id="2">Read DOM page</li>
<li class="task" data-id="3">Write code</li>
</ul>
<button class="add">Add task</button>
</section>Going up: parentNode and parentElement
Every node has a parent. Use parentElement when you specifically want an element back (it returns null if the parent is the document itself).
const li = document.querySelector(".task");
li.parentElement; // the <ul class="list">
li.parentElement.parentElement; // the <section id="todo">
document.documentElement.parentNode; // the document
document.documentElement.parentElement; // nullGoing down: children, childNodes, first/last
There are two slightly different ways to ask for children:
element.children— only element children, as a liveHTMLCollection.element.childNodes— every child node, including text nodes for whitespace, comments, etc.element.firstElementChild/element.lastElementChild— first/last element child, ornullif none.element.firstChild/element.lastChild— first/last node, often a whitespace text node.
const list = document.querySelector(".list");
list.children.length; // 3
list.firstElementChild; // <li>Buy milk</li>
list.lastElementChild; // <li>Write code</li>
list.children[1].textContent;// "Read DOM page"
list.childNodes.length; // often more than 3 — whitespace text countsGoing sideways: siblings
const middle = document.querySelector('[data-id="2"]');
middle.previousElementSibling; // <li data-id="1">Buy milk</li>
middle.nextElementSibling; // <li data-id="3">Write code</li>
const first = middle.parentElement.firstElementChild;
first.previousElementSibling; // null — already firstAs with children, the *ElementSibling variants skip text and comment nodes. The plain previousSibling / nextSibling walk over every node.
Closest: walking up until something matches
element.closest(selector) walks up from the element itself, looking for the nearest ancestor that matches a CSS selector — including the element itself. It returns null if nothing matches.
click delegation, briefly
document.querySelector("#todo").addEventListener("click", (e) => {
const task = e.target.closest(".task");
if (!task) return; // clicked the heading, the button, etc.
console.log("clicked task id =", task.dataset.id);
});clicked task id = 2 (when the user clicks the second <li>)
This is the workhorse of event delegation — covered in detail on its own page.
Matches: is this element a …?
element.matches(selector) returns true if the element itself satisfies the selector. It does not look at ancestors.
const li = document.querySelector(".task.done");
li.matches(".task"); // true
li.matches(".done"); // true
li.matches("li:nth-child(2)"); // true
li.matches("button"); // falsematches pairs well with closest and with event delegation when you want to be specific about which sub-elements you care about.
Putting it together: a tiny traversal
from a clicked button to its task
document.querySelector(".add").addEventListener("click", (e) => {
const section = e.target.closest("section");
const list = section.querySelector(".list");
const lastTask = list.lastElementChild;
console.log("about to add after:", lastTask?.textContent ?? "(empty list)");
});Performance note
Walking the tree with
parentElement,children, and siblings is faster than re-runningquerySelectorfromdocumentfor every step.closestis implemented natively and is essentially free — use it liberally.Live collections (
children) are recomputed lazily, but iterating them while you mutate the tree still leads to bugs. Snapshot withArray.from(parent.children)when in doubt.
Quick reference
Up —
parentElement,closest(sel)Down —
children,firstElementChild,lastElementChild,querySelector(sel)Sideways —
previousElementSibling,nextElementSiblingTest self —
matches(sel)
With these six tools you can navigate any DOM tree without re-querying document. The next pages put them to work changing what those nodes contain and look like.