javascript-today

DOM Traversal

DOM traversal is navigating the document tree to find elements relative to other elements. Instead of searching the entire document, traverse from known elements to their relatives.

The DOM Tree Structure

HTML creates a tree of nodes:

<div id="container">
  <h2>Title</h2>
  <p>First paragraph</p>
  <p>Second paragraph</p>
</div>
container (div)
├── h2 (Title)
├── p (First paragraph)
└── p (Second paragraph)

Parent Elements

parentElement

Get the parent element:

const paragraph = document.querySelector('p');

// Get parent
const parent = paragraph.parentElement;
console.log(parent.tagName); // "DIV"

// Chain to go up multiple levels
const grandparent = paragraph.parentElement.parentElement;

parentNode vs parentElement

const element = document.querySelector('#child');

// Usually the same
console.log(element.parentElement); // <div>
console.log(element.parentNode);    // <div>

// Difference: document has no parentElement
console.log(document.documentElement.parentElement); // null
console.log(document.documentElement.parentNode);    // #document

Use parentElement - more intuitive for regular elements.

closest()

Find the nearest ancestor matching a selector:

const button = document.querySelector('button');

// Find closest form
const form = button.closest('form');

// Find closest parent with class
const card = button.closest('.card');

// Find closest with data attribute
const container = button.closest('[data-container]');

// Returns null if not found
const modal = button.closest('.modal'); // null if not in modal

Very useful for event delegation:

document.addEventListener('click', (e) => {
  // Check if click was on a delete button
  const deleteButton = e.target.closest('.delete-button');
  
  if (deleteButton) {
    // Find which card the button belongs to
    const card = deleteButton.closest('.card');
    const cardId = card.dataset.id;
    
    deleteCard(cardId);
  }
});

Child Elements

children

Get all child elements (not text nodes):

const container = document.querySelector('#container');

// Get all children
const children = container.children; // HTMLCollection
console.log(children.length); // 3

// Access by index
const firstChild = container.children[0];
const secondChild = container.children[1];

// Convert to array for iteration
Array.from(children).forEach(child => {
  console.log(child.tagName);
});

firstElementChild / lastElementChild

const list = document.querySelector('ul');

// Get first and last child elements
const first = list.firstElementChild;
const last = list.lastElementChild;

console.log(first.textContent); // First item
console.log(last.textContent);  // Last item

childNodes (includes text nodes)

const div = document.querySelector('div');

// Get all nodes (including text, comments)
const nodes = div.childNodes;

// Usually you want children instead
const elements = div.children;

Prefer children over childNodes for elements.

childElementCount

const list = document.querySelector('ul');

// Count child elements
const count = list.childElementCount;
console.log(`List has ${count} items`);

// Same as
console.log(list.children.length);

Sibling Elements

nextElementSibling

Get the next sibling element:

const paragraph = document.querySelector('p');

// Get next paragraph
const next = paragraph.nextElementSibling;

// Chain to skip elements
const afterNext = paragraph.nextElementSibling.nextElementSibling;

// Returns null if no next sibling
const last = document.querySelector('p:last-child');
console.log(last.nextElementSibling); // null

previousElementSibling

Get the previous sibling:

const paragraph = document.querySelector('p:nth-child(3)');

// Get previous paragraph
const previous = paragraph.previousElementSibling;

// Returns null if no previous sibling
const first = document.querySelector('p:first-child');
console.log(first.previousElementSibling); // null

Practical Sibling Navigation

// Highlight current and adjacent items
const currentItem = document.querySelector('.current');

if (currentItem.previousElementSibling) {
  currentItem.previousElementSibling.classList.add('before-current');
}

if (currentItem.nextElementSibling) {
  currentItem.nextElementSibling.classList.add('after-current');
}

// Get all siblings
function getAllSiblings(element) {
  const siblings = [];
  let sibling = element.parentElement.firstElementChild;
  
  while (sibling) {
    if (sibling !== element) {
      siblings.push(sibling);
    }
    sibling = sibling.nextElementSibling;
  }
  
  return siblings;
}

Traversal Methods Comparison

<div id="container">
  <h2>Title</h2>
  <p id="first">Paragraph 1</p>
  <p id="second">Paragraph 2</p>
  <p id="third">Paragraph 3</p>
</div>
const second = document.querySelector('#second');

// Parent
second.parentElement;              // <div id="container">
second.closest('#container');      // <div id="container">

// Children (from container)
const container = document.querySelector('#container');
container.children;                // [h2, p, p, p]
container.firstElementChild;       // <h2>
container.lastElementChild;        // <p id="third">
container.children[1];             // <p id="first">

// Siblings
second.previousElementSibling;     // <p id="first">
second.nextElementSibling;         // <p id="third">

Practical Examples

Tab Navigation

const tabs = document.querySelectorAll('.tab');

tabs.forEach(tab => {
  tab.addEventListener('click', () => {
    // Hide all tab contents
    const tabContainer = tab.closest('.tab-container');
    const allContents = tabContainer.querySelectorAll('.tab-content');
    allContents.forEach(content => content.classList.add('hidden'));
    
    // Remove active class from all tabs
    tabs.forEach(t => t.classList.remove('active'));
    
    // Show clicked tab content
    tab.classList.add('active');
    const tabId = tab.dataset.tab;
    const content = document.querySelector(`#${tabId}`);
    content.classList.remove('hidden');
  });
});

Accordion

const accordionHeaders = document.querySelectorAll('.accordion-header');

accordionHeaders.forEach(header => {
  header.addEventListener('click', () => {
    // Get the content panel (next sibling)
    const content = header.nextElementSibling;
    
    // Toggle content
    content.classList.toggle('open');
    
    // Toggle header active state
    header.classList.toggle('active');
  });
});

Nested List Navigation

const nestedList = document.querySelector('#nested-list');

nestedList.addEventListener('click', (e) => {
  if (e.target.tagName === 'LI') {
    const li = e.target;
    
    // Find nested ul (child)
    const nestedUl = li.querySelector('ul');
    
    if (nestedUl) {
      nestedUl.classList.toggle('expanded');
    }
    
    // Prevent event from bubbling to parent li
    e.stopPropagation();
  }
});
function createBreadcrumb(element) {
  const breadcrumb = [];
  let current = element;
  
  // Traverse up to body
  while (current && current !== document.body) {
    breadcrumb.unshift({
      tag: current.tagName,
      id: current.id,
      classes: Array.from(current.classList)
    });
    
    current = current.parentElement;
  }
  
  return breadcrumb;
}

const element = document.querySelector('#target');
console.log(createBreadcrumb(element));
// [
//   { tag: 'HTML', id: '', classes: [] },
//   { tag: 'BODY', id: '', classes: [] },
//   { tag: 'DIV', id: 'container', classes: ['wrapper'] },
//   { tag: 'P', id: 'target', classes: ['text'] }
// ]

Form Field Dependencies

const countrySelect = document.querySelector('#country');

countrySelect.addEventListener('change', () => {
  // Find the form
  const form = countrySelect.closest('form');
  
  // Find state field (next input after country)
  const stateField = countrySelect
    .parentElement
    .nextElementSibling
    .querySelector('select');
  
  if (countrySelect.value === 'US') {
    stateField.disabled = false;
    stateField.required = true;
  } else {
    stateField.disabled = true;
    stateField.required = false;
  }
});

Table Row Actions

const table = document.querySelector('#data-table');

table.addEventListener('click', (e) => {
  // Find if click was on delete button
  const deleteBtn = e.target.closest('.delete-btn');
  
  if (deleteBtn) {
    // Get the row
    const row = deleteBtn.closest('tr');
    
    // Get data from cells
    const cells = row.children;
    const id = cells[0].textContent;
    const name = cells[1].textContent;
    
    if (confirm(`Delete ${name}?`)) {
      row.remove();
      console.log(`Deleted row ${id}`);
    }
  }
});

Slideshow Navigation

const slides = document.querySelectorAll('.slide');
let currentSlide = document.querySelector('.slide.active');

function nextSlide() {
  const next = currentSlide.nextElementSibling;
  
  if (next && next.classList.contains('slide')) {
    currentSlide.classList.remove('active');
    next.classList.add('active');
    currentSlide = next;
  } else {
    // Wrap to first slide
    currentSlide.classList.remove('active');
    slides[0].classList.add('active');
    currentSlide = slides[0];
  }
}

function previousSlide() {
  const prev = currentSlide.previousElementSibling;
  
  if (prev && prev.classList.contains('slide')) {
    currentSlide.classList.remove('active');
    prev.classList.add('active');
    currentSlide = prev;
  } else {
    // Wrap to last slide
    currentSlide.classList.remove('active');
    const lastSlide = slides[slides.length - 1];
    lastSlide.classList.add('active');
    currentSlide = lastSlide;
  }
}

Combining querySelector with Traversal

Often combine both approaches:

const button = document.querySelector('#save-button');

// Traverse to find form
const form = button.closest('form');

// Query within form
const emailInput = form.querySelector('input[type="email"]');
const allInputs = form.querySelectorAll('input');

// Traverse from input
const label = emailInput.previousElementSibling;
const errorMsg = emailInput.nextElementSibling;

Performance Considerations

// ✅ GOOD: Store reference
const container = document.querySelector('#container');
const children = container.children;

for (let i = 0; i < children.length; i++) {
  children[i].classList.add('styled');
}

// ❌ SLOW: Query every iteration
for (let i = 0; i < 100; i++) {
  const item = document.querySelector(`#item-${i}`);
  item.style.color = 'red';
}

// ✅ BETTER: Traverse from parent
const list = document.querySelector('#list');
Array.from(list.children).forEach(item => {
  item.style.color = 'red';
});

Best Practices

DO:

  • Use parentElement, children, nextElementSibling for elements
  • Use closest() for finding ancestor with selector
  • Store element references to avoid repeated queries
  • Use traversal within a known subtree instead of querying whole document

DON’T:

  • Use parentNode when you want parentElement
  • Use childNodes when you want children
  • Query the entire document when you can traverse locally
  • Forget to check for null when traversing

Summary

Direction Property Method
Parent parentElement Get direct parent
closest(selector) Find ancestor matching selector
Children children Get all child elements
firstElementChild Get first child
lastElementChild Get last child
Siblings nextElementSibling Get next sibling
previousElementSibling Get previous sibling

Common patterns:

element.parentElement              // Go up one level
element.closest('.card')           // Find ancestor
element.children[0]                // First child
element.nextElementSibling         // Next sibling
element.querySelector('selector')  // Search down