Academic Block

DOM Traversal Get Certified

Once you can select an element, the next step is often to move relative to it — up to a parent, down to children, or sideways to siblings. This is called DOM traversal.

In this lesson you will learn the most useful traversal properties and methods: parentElement, children, firstElementChild, nextElementSibling, closest(), contains(), and scoped querySelector.

Moving Up With parentElement

parentElement returns the element that directly contains the current one, or null if there is none.

What this code does: Every element (except the root) has a parentElement. Walking up is how you leave a nested structure.

function makeElement(tag) {
    return {
        tag: tag,
        textContent: "",
        parentElement: null
    };
}

const article = makeElement("article");
const paragraph = makeElement("p");
paragraph.textContent = "Hello";
paragraph.parentElement = article;

console.log(paragraph.parentElement.tag); // "article"
console.log(article.parentElement);       // null

Moving Down With children

children is a live collection of an element’s direct element children (no text nodes).

What this code does: children lists only element children. Useful when you need every direct child of a container.

function makeElement(tag) {
    return {
        tag: tag,
        textContent: "",
        children: []
    };
}

const list = makeElement("ul");
const item1 = makeElement("li");
item1.textContent = "Apples";
const item2 = makeElement("li");
item2.textContent = "Bananas";

list.children.push(item1, item2);

console.log(list.children.length);               // 2
console.log(list.children[0].textContent);       // "Apples"

firstElementChild and lastElementChild

What this code does: Shortcuts to the first and last element children of a parent.

function makeElement(tag) {
    return { tag: tag, textContent: "", children: [] };
}

const nav = makeElement("nav");
const home = makeElement("a");
home.textContent = "Home";
const about = makeElement("a");
about.textContent = "About";
const contact = makeElement("a");
contact.textContent = "Contact";

nav.children.push(home, about, contact);

console.log(nav.children[0].textContent);                    // first
console.log(nav.children[nav.children.length - 1].textContent); // last

Moving Sideways With Siblings

nextElementSibling and previousElementSibling let you step from one element to the one right beside it.

What this code does: Sibling links connect elements that share a parent. nextElementSibling and previousElementSibling move sideways.

function makeElement(tag) {
    return {
        tag: tag,
        textContent: "",
        parentElement: null,
        nextElementSibling: null,
        previousElementSibling: null
    };
}

function appendChild(parent, child) {
    child.parentElement = parent;
    const lastChild = parent.children[parent.children.length - 1];
    if (lastChild) {
        lastChild.nextElementSibling = child;
        child.previousElementSibling = lastChild;
    }
    parent.children.push(child);
}

const tabBar = { children: [] };

const tabOne = makeElement("button");
tabOne.textContent = "Overview";
const tabTwo = makeElement("button");
tabTwo.textContent = "Specifications";
const tabThree = makeElement("button");
tabThree.textContent = "Reviews";

appendChild(tabBar, tabOne);
appendChild(tabBar, tabTwo);
appendChild(tabBar, tabThree);

console.log(tabTwo.previousElementSibling.textContent);
console.log(tabTwo.nextElementSibling.textContent);

Node Properties vs. Element Properties

On a real page, properties like childNodes, firstChild, and nextSibling also exist, but they include every type of node — including whitespace text nodes created by line breaks and indentation in your HTML. The Element versions (children, firstElementChild, nextElementSibling) skip those text nodes and only include actual elements, which is usually what you want.

Node Version (includes text nodes) Element Version (elements only)
childNodes children
firstChild firstElementChild
lastChild lastElementChild
nextSibling nextElementSibling
previousSibling previousElementSibling

In most everyday code, the Element versions are the ones you’ll want, since dealing with unexpected whitespace text nodes is rarely useful.

Searching Within a Specific Element

querySelector() and querySelectorAll() aren’t limited to the whole document — calling them on a specific element searches only within that element’s descendants.

What this code does: Scoped querySelectorAll searches only inside a container, not the whole document.

function makeElement(tag, className) {
    return {
        tag: tag,
        className: className || "",
        children: [],
        appendChild(child) { this.children.push(child); },
        querySelectorAll(selector) {
            const matches = [];
            function search(element) {
                element.children.forEach(function (child) {
                    if ("." + child.className === selector) {
                        matches.push(child);
                    }
                    search(child);
                });
            }
            search(this);
            return matches;
        }
    };
}

const orderSummary = makeElement("section");
const lineItemsContainer = makeElement("div");
orderSummary.appendChild(lineItemsContainer);

["Keyboard", "Mouse", "Monitor"].forEach(function (itemName) {
    const lineItem = makeElement("div", "line-item");
    lineItem.name = itemName;
    lineItemsContainer.appendChild(lineItem);
});

const lineItems = orderSummary.querySelectorAll(".line-item");
console.log(lineItems.map(item => item.name));

Finding an Ancestor With closest()

closest() searches upward from an element, through its parents, and returns the nearest ancestor (or the element itself) that matches a given selector. It’s especially useful inside click handlers, to find a containing element from wherever inside it was clicked.

What this code does: closest() walks up the tree until it finds an ancestor that matches a selector (or returns null).

function makeElement(tag, className) {
    return {
        tag: tag,
        className: className || "",
        parentElement: null,
        closest(selector) {
            let current = this;
            while (current) {
                if ("." + current.className === selector) {
                    return current;
                }
                current = current.parentElement;
            }
            return null;
        }
    };
}

function appendChild(parent, child) {
    child.parentElement = parent;
}

const productCard = makeElement("article", "product-card");
const priceTag = makeElement("span", "price-tag");
appendChild(productCard, priceTag);

const foundCard = priceTag.closest(".product-card");
console.log(foundCard === productCard);

Checking Containment With contains()

contains() checks whether one element is a descendant of another (or is the element itself), which is useful for things like detecting clicks outside of a dropdown menu.

What this code does: contains() checks whether one element is nested inside another — useful for “click outside” logic.

function makeElement(tag) {
    return {
        tag: tag,
        children: [],
        appendChild(child) { this.children.push(child); },
        contains(target) {
            if (this === target) return true;
            return this.children.some(function (child) {
                return child.contains ? child.contains(target) : child === target;
            });
        }
    };
}

const dropdownMenu = makeElement("div");
const menuOption = makeElement("li");
dropdownMenu.appendChild(menuOption);
const unrelatedButton = makeElement("button");

console.log(dropdownMenu.contains(menuOption));
console.log(dropdownMenu.contains(unrelatedButton));

Practical Example

Let’s use traversal to highlight a single selected tab and reset its siblings, a common pattern in tabbed interfaces.

What this code does: Builds a tab bar and demonstrates selecting a tab and relating it to its neighbors in the same parent.

function makeElement(tag) {
    return {
        tag: tag,
        textContent: "",
        parentElement: null,
        selected: false
    };
}

function buildTabBar(labels) {
    const tabBar = { children: [] };
    labels.forEach(function (label) {
        const tab = makeElement("button");
        tab.textContent = label;
        tab.parentElement = tabBar;
        tabBar.children.push(tab);
    });
    return tabBar;
}

function selectTab(tabBar, tabToSelect) {
    tabBar.children.forEach(function (tab) {
        tab.selected = (tab === tabToSelect);
    });
}

const tabBar = buildTabBar(["Overview", "Specifications", "Reviews"]);
selectTab(tabBar, tabBar.children[1]);

tabBar.children.forEach(function (tab) {
    console.log(tab.textContent, "-> selected:", tab.selected);
});

Common Mistakes With DOM Traversal

  • Using childNodes or firstChild when children or firstElementChild was intended, and being surprised by unexpected text nodes.
  • Forgetting that parentElement is null for the topmost element, and not checking for that before using it further.
  • Assuming nextElementSibling will always return something, when it returns null for the last element in a group.
  • Using closest() with a selector that doesn’t match anything in the element’s ancestor chain, and not handling the resulting null.
  • Calling querySelector() on the whole document when searching within a specific container would be faster and more precise.

Best Practices for DOM Traversal

  • Prefer the Element-specific properties (children, firstElementChild, nextElementSibling) over their Node counterparts in most everyday code.
  • Always check for null before continuing to traverse, since reaching the edge of the tree is common.
  • Use closest() inside event handlers to reliably find a containing element, regardless of exactly where inside it was clicked.
  • Scope querySelector() calls to the smallest relevant container rather than always searching the entire document.
  • Use contains() for reliable “is this inside that?” checks instead of manually walking up the tree yourself.

Quick Summary

  • parentElement moves up to a containing element.
  • children, firstElementChild, and lastElementChild move down to child elements.
  • nextElementSibling and previousElementSibling move sideways between elements that share a parent.
  • Element-specific properties skip whitespace text nodes; their Node counterparts do not.
  • closest() searches upward for a matching ancestor; contains() checks whether one element is nested inside another.
  • querySelector() and querySelectorAll() can be scoped to search within a specific element instead of the whole page.

Try It Yourself

Modify the code below. Try selecting a different tab, or logging each tab’s neighbors using the sibling links.

What this code does: Practice example: a short step list linked by siblings. Try changing the steps or logging more neighbors, then click Test My Code.

function makeElement(tag) {
    return {
        tag: tag,
        textContent: "",
        nextElementSibling: null,
        previousElementSibling: null
    };
}

const step1 = makeElement("li");
step1.textContent = "Sign up";
const step2 = makeElement("li");
step2.textContent = "Verify email";
const step3 = makeElement("li");
step3.textContent = "Complete profile";

step1.nextElementSibling = step2;
step2.previousElementSibling = step1;
step2.nextElementSibling = step3;
step3.previousElementSibling = step2;

console.log("Step after 'Sign up':", step1.nextElementSibling.textContent);
console.log("Step before 'Complete profile':", step3.previousElementSibling.textContent);
Tip: When working with click events, reach for closest() instead of manually checking event.target and its parents one by one — it’s built exactly for that “find the containing thing” problem, and it’s far less error-prone.

Understanding DOM traversal is essential before moving on to event delegation, forms, and more advanced browser-based JavaScript concepts.

🧪 Test Your JavaScript Code

Edit the code on the left and click “Run Code” to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click “Run Code” to see the output here.

Ctrl + Enter to run code Esc to close