Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~19 min· topic 7 of 8

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 null if 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 null at 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.

index.htmlwhole filehtml
<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.

See the family linksdiagram
Rendering diagram…

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.

app.jswhole filejavascript
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.

app.jswhole filejavascript
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.

app.jswhole filejavascript
function countElements(el) {
  let count = 1;
  for (const child of el.children) {
    count += countElements(child);
  }
  return count;
}

Try it yourself

  1. 1

    Swap to the node family

    In the first example, change previousElementSibling and nextElementSibling to previousSibling and nextSibling. Predict the output first.

  2. 2

    Count every element

    In the second example, write countElements(el) (from the walkthrough) and log countElements(document.getElementById("site")). Count the tags in the HTML yourself first.

  3. 3

    Use a TreeWalker

    In the second example, add const walker = document.createTreeWalker(document.getElementById("site"), NodeFilter.SHOW_ELEMENT); and loop with while (walker.nextNode()) console.log(walker.currentNode.tagName);. Compare the order with outline. (The walker starts at the root and nextNode() moves past it, so NAV isn't printed.)

Code & diagrams

Parents, children and siblings New tab

`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

Press Run to see the page.

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
Walking a whole tree with recursion New tab

`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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

nav
| ul
| | li
| | | a
| | li
| | | a
| | | ul
| | | | li
| | | | | a
| | | | li
| | | | | a
Chairs is 5 levels deep
Up, then down: reading a row from its button New tab

Each 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Lamp costs 499 (column 3)
Chair costs 1299 (column 3)
Chair

Break 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.

terminal
$ the page shows
── what you'll see ──
The second item doesn't change. Instead, the whitespace between the items is replaced by the word Done, squeezed between the first two items.

Break #2

Chain parentElement several times

Write button.parentElement.parentElement.querySelector(".price"), then a designer wraps the button in a <div class="actions">.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'textContent')

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.

  • ▸

    parentElement and parentNode only differ at the top (<html> has the document as parentNode) and for nodes inside a DocumentFragment or shadow root, where parentNode is the fragment and parentElement is null. In shadow DOM, walking up from inside stops at the shadow root; use getRootNode().host to reach the host element.

  • ▸

    node.compareDocumentPosition(other) returns a bitmask telling you whether other comes before, after, contains or is contained by node. 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 (/dsa iterative DFS) avoids call stack limits on generated or pathological trees.

  • ▸

    el.children and childNodes are 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. 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. 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, plus childElementCount. Because indented HTML puts whitespace text nodes between tags (Topic 8.1), you almost always want the element family.

  3. 3

    Going up. el.parentElement is the element that contains el. parentNode is almost the same, but for <html> it returns document (a node that isn't an element), while parentElement returns null. To go up several levels to a particular kind of ancestor, use el.closest(selector) (Topic 8.2) instead of chaining parentElement.parentElement.parentElement, which breaks the moment someone adds a wrapper <div>.

  4. 4

    Going down. el.children is a live HTMLCollection of child elements; firstElementChild and lastElementChild are the ends. These only look one level down. To reach deeper descendants, use el.querySelector(...) or walk the tree yourself. parent.contains(other) answers whether other is anywhere inside parent (or is parent itself): handy for "was the click inside the menu?" checks.

  5. 5

    Going sideways. nextElementSibling and previousElementSibling move to neighbouring elements with the same parent, and return null at the ends. A loop such as for (let el = list.firstElementChild; el; el = el.nextElementSibling) visits every child in order and stops when it reaches null.

  6. 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 /dsa course apply directly. The browser also has built-in walkers: document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT) gives you nextNode() to step through every element in document order without recursion.

Explain it without notes

01

What is the difference between nextSibling and nextElementSibling? Why does it matter?

02

How would you find the table row that a clicked button is in, and why is closest better than chaining parentElement?

03

How do you visit every element inside a container, at any depth?

04

What is the difference between parentNode and parentElement?

Practice

01

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.

02

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. closest plus a scoped querySelector gives 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 closest to climb to a meaningful ancestor instead of chaining parentElement.

  • I can check whether one element is inside another with contains.

  • I can walk a whole subtree with recursion or a TreeWalker.