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.

The classic bug: el.firstChild on nicely indented markup returns a text node holding a newline and some spaces, and the next property access on what you assumed was an element comes back undefined. If you meant the first tag, say firstElementChild.

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