1. Interaction
  2. APIs
  3. DOM
  4. Walking the tree

Step 03

Walking the tree

A selector jumps you straight to a node. But once you are standing on one, the tree offers a second way to move: by relationship. Every node knows its parent, its children, and its neighbors on either side, and those connections are properties you can follow. Walking is how you make relative moves — and the properties come in two flavors, because of those whitespace text nodes from step 1.

Learning Objectives

  • Move between nodes with parent, child, and sibling properties
  • Explain why firstChild and firstElementChild differ, and when it bites
  • Walk upward to a matching ancestor with closest()
  • Traverse a subtree depth-first with firstChild / nextSibling

Two flavors of every relationship

Indent your markup like a civilized person and every element grows whitespace-only text children. So the tree API splits each relationship in two: the node properties see everything — elements, text, comments — and the element properties skip to tags only.

const outer = document.querySelector('#outer'); outer.firstChild; // a whitespace text node (nodeType 3, #text) outer.firstElementChild; // the first actual element (nodeType 1) outer.childNodes; // every child node, text included outer.children; // elements only li2.previousElementSibling; // #li1 (li2.previousSibling would be text) li2.nextElementSibling; // #li3

When you are working with page structure — which is most of the time — use the element flavor and the whitespace never troubles you. Reach for the node flavor only when text nodes are the point, and then check nodeType: 1 is an element, 3 is text, 8 is a comment.

Walking up, and walking everything

parentNode climbs one level. closest() climbs with a selector — from the current element upward, returning the first ancestor (or self) that matches:

document.querySelector('#li2').closest('div'); // nearest enclosing <div>

That one call is the backbone of event delegation in the Events series: from whatever small thing was clicked, walk up to the component that owns it. And with just firstChild and nextSibling you can visit an entire subtree — the depth-first walk every serializer, sanitizer, and framework diff is built on:

function dfs(node, depth = 0) { const indent = ' '.repeat(depth); lines.push(`${indent}- ${node.nodeName}`); node = node.firstChild; while (node) { dfs(node, depth + 1); node = node.nextSibling; } }

Live demo

A small nested tree — a list inside a div inside a div — with each traversal one button press. Whatever a walk reaches gets marked in the sample, so you can check your prediction against the tree. Pay attention to section 2: firstChild versus firstElementChild on the same element, and the childNodes dump where the whitespace text nodes finally show themselves. Then run the depth-first walk and read the indented output against the markup.

Walk or query?

Both reach nodes; they answer different questions. A query answers “find me the nodes matching this description, wherever they are.” A walk answers “from the node I am holding, give me its neighbor.” Inside a delegated event handler, holding the element that was clicked, closest() and children are the natural moves — re-querying the whole document to find where you already are is the roundabout version.

Next Steps

You can reach any node in the tree. In Step 4: Attributes and mappings, we start changing what we find:

  • Read and write attributes with getAttribute / setAttribute
  • Meet the property mapping — and the renamed reserved words like className and htmlFor
  • Flip visual state with classList
  • Attach your own data with data-* and read it back through dataset