Topic 8.7
Traversing the DOM
In one line
From any element you can move around the tree: up with parentElement and closest, down with children, firstElementChild and lastElementChild, and sideways with previousElementSibling and nextElementSibling; the element-only versions skip the whitespace text nodes that the plain node versions include.
Think of it like this
Walking around a block of flats. From your flat you can go up to the floor above, down to the flats below, or next door to the neighbours on the same floor. You don't need the building's full address list every time; you start where you are and move one step at a time. Traversing the DOM is the same: start from an element you already have and step to its parent, children or siblings.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Traverse
- Move through a structure step by step, here from one DOM node to related nodes.
- Ancestor / descendant
- Any node above you in the tree (parent, grandparent...) / any node below you (child, grandchild...).
- parentElement
- The element directly containing this one, or
nullif there isn't one. - children
- A live list of this element's child elements, one level down only.
- nextElementSibling
- The next element with the same parent, or
nullat the end. - contains
- A method that says whether a node is inside another node (or is that node).
- Recursion
- A function that calls itself, used to visit every level of a tree.
- TreeWalker
- A built-in browser tool that steps through the nodes of a subtree in order.
Step by step
01Start from an element you have
Here we hold the middle item of a list. Everything else is reached from it, without searching the document again.
<ul id="steps">
<li id="one">Wake up</li>
<li id="two">Brush teeth</li>
<li id="three">Eat breakfast</li>
</ul>02See the family links
Each arrow is one property. The element family skips the whitespace text between the <li> tags.
03Loop over siblings
Start at the first child and step to the next sibling until there isn't one. This is the same as looping over children, written as a chain.
const steps = document.getElementById("steps");
for (let li = steps.firstElementChild; li; li = li.nextElementSibling) {
console.log(li.textContent);
}04Climb to the right container
Use closest to jump to the ancestor you mean, then search down from there. This "up, then down" move is the most common traversal in real code.
function priceFor(button) {
const row = button.closest("tr"); // up to the row
return row.querySelector(".price").textContent; // down to the price cell
}05Walk the whole tree
A recursive function visits an element, then each child, then each child's children, so it reaches every element below the start, however deep.
function countElements(el) {
let count = 1;
for (const child of el.children) {
count += countElements(child);
}
return count;
}Try it yourself
- 1
Swap to the node family
In the first example, change
previousElementSiblingandnextElementSiblingtopreviousSiblingandnextSibling. Predict the output first. - 2
Count every element
In the second example, write
countElements(el)(from the walkthrough) and logcountElements(document.getElementById("site")). Count the tags in the HTML yourself first. - 3
Use a TreeWalker
In the second example, add
const walker = document.createTreeWalker(document.getElementById("site"), NodeFilter.SHOW_ELEMENT);and loop withwhile (walker.nextNode()) console.log(walker.currentNode.tagName);. Compare the order withoutline. (The walker starts at the root andnextNode()moves past it, soNAVisn't printed.)
Code & diagrams
`previousSibling` found the whitespace text node (a line break and two spaces) between the `<li>` tags; `previousElementSibling` skipped it. At the top, `parentNode` of `<html>` is the document, but `parentElement` is `null`.
Preview
Console
Nothing logged yet.Expected console output
<ul#steps>
<li#one> <li#three>
#text '\n '
Wake up / Eat breakfast
3 7 3
null
#document null
true false`outline` goes down (each call handles one element, then its children); `depthOf` goes up with `parentElement` until it reaches the root. This is depth-first traversal from the `/dsa` course, on a real tree.
Preview
Console
Nothing logged yet.Expected console output
nav
| ul
| | li
| | | a
| | li
| | | a
| | | ul
| | | | li
| | | | | a
| | | | li
| | | | | a
Chairs is 5 levels deepEach button found its own row with `closest`, then searched down inside that row only. The same function works for any number of rows. In Phase 9 this becomes the click handler.
Preview
Console
Nothing logged yet.Expected console output
Lamp costs 499 (column 3)
Chair costs 1299 (column 3)
ChairBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use nextSibling to reach the next item
Write document.getElementById("one").nextSibling.textContent = "Done"; for an indented list.
Break #2
Chain parentElement several times
Write button.parentElement.parentElement.querySelector(".price"), then a designer wraps the button in a <div class="actions">.
Myth vs fact
Myth
firstChild gives the first element.
Fact
It gives the first node, which is often a whitespace text node. Use firstElementChild.
Myth
children includes all descendants.
Fact
It includes only direct children, one level down. Use querySelectorAll or a recursive walk to reach deeper elements.
Myth
Traversal is faster than querying, so always traverse.
Fact
Both are fast. Traverse when you already hold a nearby element and the relationship is meaningful (closest); query when you're looking for something by what it is. Readability matters more.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
parentElementandparentNodeonly differ at the top (<html>has the document asparentNode) and for nodes inside a DocumentFragment or shadow root, whereparentNodeis the fragment andparentElementisnull. In shadow DOM, walking up from inside stops at the shadow root; usegetRootNode().hostto reach the host element. - ▸
node.compareDocumentPosition(other)returns a bitmask telling you whetherothercomes before, after, contains or is contained bynode. It's how libraries sort nodes into document order or check positions without walking. - ▸
Recursion depth isn't a concern for normal pages (DOM trees are rarely more than a few dozen levels deep), but a TreeWalker or an explicit stack (
/dsaiterative DFS) avoids call stack limits on generated or pathological trees. - ▸
el.childrenandchildNodesare live, so changing the tree while iterating them can skip or repeat items. When removing or moving children in a loop, iterate over a copy:for (const child of [...el.children]).
Remember this
- 1
Traversing means moving from one node to related nodes instead of searching the whole document again. It's useful when you already hold an element (often the one a user clicked, in Phase 9) and need its neighbours: the row a button sits in, the label next to an input, the next item in a list. It's also how you process a whole tree, for example to build a table of contents.
- 2
There are two families of properties. The node family includes everything, including text and comments:
parentNode,childNodes,firstChild,lastChild,previousSibling,nextSibling. The element family skips text and comment nodes:parentElement,children,firstElementChild,lastElementChild,previousElementSibling,nextElementSibling, pluschildElementCount. Because indented HTML puts whitespace text nodes between tags (Topic 8.1), you almost always want the element family. - 3
Going up.
el.parentElementis the element that containsel.parentNodeis almost the same, but for<html>it returnsdocument(a node that isn't an element), whileparentElementreturnsnull. To go up several levels to a particular kind of ancestor, useel.closest(selector)(Topic 8.2) instead of chainingparentElement.parentElement.parentElement, which breaks the moment someone adds a wrapper<div>. - 4
Going down.
el.childrenis a live HTMLCollection of child elements;firstElementChildandlastElementChildare the ends. These only look one level down. To reach deeper descendants, useel.querySelector(...)or walk the tree yourself.parent.contains(other)answers whetherotheris anywhere insideparent(or isparentitself): handy for "was the click inside the menu?" checks. - 5
Going sideways.
nextElementSiblingandpreviousElementSiblingmove to neighbouring elements with the same parent, and returnnullat the ends. A loop such asfor (let el = list.firstElementChild; el; el = el.nextElementSibling)visits every child in order and stops when it reachesnull. - 6
Walking a whole subtree is a classic use of recursion (Topic 3.8): visit a node, then call the same function for each of its children. The DOM is a real tree data structure, so the depth-first and breadth-first traversals from Hectal's
/dsacourse apply directly. The browser also has built-in walkers:document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT)gives younextNode()to step through every element in document order without recursion.
Explain it without notes
What is the difference between nextSibling and nextElementSibling? Why does it matter?
How would you find the table row that a clicked button is in, and why is closest better than chaining parentElement?
How do you visit every element inside a container, at any depth?
What is the difference between parentNode and parentElement?
Practice
Given an <input id="email"> inside a <div class="field"> that also contains a <label> before it and a <p class="error"> after it, log the label's text and the error's text by traversing from the input.
Write textOutline(el) that returns an array of the tag names of all elements inside el (not including el), in document order, using recursion. Test it on a small list.
Trade-offs
- ↔
Traversing relative to a known element is precise and fast but ties your code to the page structure; querying by class or
data-*attribute is more robust to markup changes.closestplus a scopedquerySelectorgives most of both. - ↔
A recursive walk is short and clear; a TreeWalker or explicit stack is more code but handles any depth and lets you pause, filter or resume.
Done when you can
I can move up, down and sideways with the element-family properties.
I know the node family includes whitespace text nodes and when that matters.
I use
closestto climb to a meaningful ancestor instead of chainingparentElement.I can check whether one element is inside another with
contains.I can walk a whole subtree with recursion or a TreeWalker.