DOM & BrowserFeatured

JavaScript DOM Manipulation: Complete Guide

Master DOM manipulation in JavaScript. Learn how to select, create, modify, and remove elements, handle events, and build interactive web pages.

By JavaScriptDoc Team
domjavascriptweb developmenthtmlbrowser

JavaScript DOM Manipulation: Complete Guide

The Document Object Model (DOM) is a programming interface for HTML documents. It represents the structure of a document as a tree of objects, allowing JavaScript to interact with and modify web pages dynamically.

Understanding the DOM

The DOM represents your HTML as a tree structure where each HTML element is a node.

// The DOM tree structure
// document
//   └── html
//       ├── head
//       │   ├── title
//       │   └── meta
//       └── body
//           ├── header
//           ├── main
//           └── footer

// Accessing the document object
console.log(document); // The entire document
console.log(document.documentElement); // <html> element
console.log(document.head); // <head> element
console.log(document.body); // <body> element

Selecting Elements

Modern Selection Methods

// querySelector - returns first match
const header = document.querySelector('header');
const firstButton = document.querySelector('.btn');
const submitBtn = document.querySelector('#submit');
const firstParagraph = document.querySelector('article > p');

// querySelectorAll - returns NodeList of all matches
const allButtons = document.querySelectorAll('.btn');
const allParagraphs = document.querySelectorAll('p');
const allDivs = document.querySelectorAll('div[data-role="container"]');

// Convert NodeList to Array
const buttonsArray = Array.from(allButtons);
// Or use spread operator
const paragraphsArray = [...allParagraphs];

// Complex selectors
const complexSelect = document.querySelectorAll('ul > li:nth-child(odd)');
const attributeSelect = document.querySelectorAll('input[type="text"]');
const multiClass = document.querySelectorAll('.class1.class2');

Traditional Selection Methods

// getElementById - returns single element
const mainContent = document.getElementById('main-content');

// getElementsByClassName - returns HTMLCollection
const cards = document.getElementsByClassName('card');

// getElementsByTagName - returns HTMLCollection
const allDivs = document.getElementsByTagName('div');

// getElementsByName - returns NodeList
const emailInputs = document.getElementsByName('email');

// Difference between NodeList and HTMLCollection
// HTMLCollection is live (updates automatically)
const liveCollection = document.getElementsByClassName('item');
console.log(liveCollection.length); // 3

document.body.innerHTML += '<div class="item">New Item</div>';
console.log(liveCollection.length); // 4 (automatically updated)

// NodeList from querySelectorAll is static
const staticList = document.querySelectorAll('.item');
console.log(staticList.length); // 4

document.body.innerHTML += '<div class="item">Another Item</div>';
console.log(staticList.length); // Still 4 (not updated)

Traversing the DOM

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

// Parent nodes
console.log(element.parentNode); // Direct parent
console.log(element.parentElement); // Parent element (skips non-element nodes)

// Child nodes
console.log(element.childNodes); // All child nodes (includes text nodes)
console.log(element.children); // Only element children
console.log(element.firstChild); // First child node
console.log(element.firstElementChild); // First element child
console.log(element.lastChild); // Last child node
console.log(element.lastElementChild); // Last element child

// Sibling nodes
console.log(element.previousSibling); // Previous node
console.log(element.previousElementSibling); // Previous element
console.log(element.nextSibling); // Next node
console.log(element.nextElementSibling); // Next element

// Finding ancestors
const closestSection = element.closest('section'); // Nearest ancestor matching selector
const closestWithClass = element.closest('.wrapper');

// Check if element matches selector
if (element.matches('.active')) {
  console.log('Element is active');
}

Creating and Modifying Elements

Creating Elements

// Create new elements
const newDiv = document.createElement('div');
const newParagraph = document.createElement('p');
const newButton = document.createElement('button');

// Create text nodes
const textNode = document.createTextNode('Hello World');

// Create document fragment for better performance
const fragment = document.createDocumentFragment();

// Building complex structures
function createCard(title, content) {
  const card = document.createElement('div');
  card.className = 'card';

  const cardTitle = document.createElement('h3');
  cardTitle.textContent = title;

  const cardContent = document.createElement('p');
  cardContent.textContent = content;

  card.appendChild(cardTitle);
  card.appendChild(cardContent);

  return card;
}

// Create multiple elements efficiently
const items = ['Apple', 'Banana', 'Orange'];
const list = document.createElement('ul');

items.forEach((item) => {
  const li = document.createElement('li');
  li.textContent = item;
  list.appendChild(li);
});

Modifying Element Content

const element = document.querySelector('.content');

// textContent - gets/sets text content (no HTML)
element.textContent = 'Simple text content';
console.log(element.textContent); // 'Simple text content'

// innerHTML - gets/sets HTML content
element.innerHTML = '<strong>Bold text</strong> and <em>italic text</em>';
console.log(element.innerHTML); // '<strong>Bold text</strong> and <em>italic text</em>'

// innerText - gets/sets visible text (respects styling)
element.innerText = 'Visible text only';

// Differences between textContent and innerText
const hidden = document.querySelector('.hidden-element');
hidden.innerHTML = 'Hidden <span style="display:none">Secret</span> Text';
console.log(hidden.textContent); // 'Hidden Secret Text'
console.log(hidden.innerText); // 'Hidden Text' (respects display:none)

// Safe HTML insertion
function setHTMLSafely(element, html) {
  const temp = document.createElement('div');
  temp.innerHTML = html;

  // Sanitize by removing script tags
  const scripts = temp.querySelectorAll('script');
  scripts.forEach((script) => script.remove());

  element.innerHTML = temp.innerHTML;
}

Modifying Attributes

const img = document.querySelector('img');
const link = document.querySelector('a');
const input = document.querySelector('input');

// getAttribute and setAttribute
img.setAttribute('src', 'new-image.jpg');
img.setAttribute('alt', 'Description of image');
console.log(img.getAttribute('src')); // 'new-image.jpg'

// Direct property access
img.src = 'another-image.jpg';
img.alt = 'Another description';

// Remove attributes
img.removeAttribute('title');

// Data attributes
const button = document.querySelector('button');
button.setAttribute('data-id', '123');
button.setAttribute('data-action', 'delete');

// Accessing data attributes
console.log(button.dataset.id); // '123'
console.log(button.dataset.action); // 'delete'

// Setting data attributes via dataset
button.dataset.userId = '456';
button.dataset.timestamp = Date.now();

// Boolean attributes
input.disabled = true;
input.required = true;
input.checked = false;

// Class manipulation
const div = document.querySelector('.box');

// className property
div.className = 'box active'; // Replaces all classes
div.className += ' highlighted'; // Append class

// classList API (preferred)
div.classList.add('new-class');
div.classList.remove('old-class');
div.classList.toggle('active'); // Add if not present, remove if present
div.classList.contains('active'); // Returns boolean
div.classList.replace('old', 'new'); // Replace class

// Add multiple classes
div.classList.add('class1', 'class2', 'class3');

Styling Elements

const element = document.querySelector('.box');

// Inline styles
element.style.backgroundColor = 'blue';
element.style.color = 'white';
element.style.padding = '20px';
element.style.borderRadius = '5px';

// Camel case for CSS properties
element.style.marginTop = '10px';
element.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';

// Setting multiple styles
Object.assign(element.style, {
  width: '200px',
  height: '100px',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
});

// Getting computed styles
const computedStyles = window.getComputedStyle(element);
console.log(computedStyles.backgroundColor);
console.log(computedStyles.getPropertyValue('font-size'));

// CSS custom properties (CSS variables)
element.style.setProperty('--primary-color', '#007bff');
const primaryColor = computedStyles.getPropertyValue('--primary-color');

// Remove inline styles
element.style.removeProperty('background-color');
element.style.backgroundColor = ''; // Alternative way

// Toggle visibility
function toggleVisibility(element) {
  if (element.style.display === 'none') {
    element.style.display = '';
  } else {
    element.style.display = 'none';
  }
}

Adding and Removing Elements

// Adding elements
const container = document.querySelector('.container');
const newElement = document.createElement('div');

// appendChild - adds to end
container.appendChild(newElement);

// insertBefore - insert before specific element
const referenceElement = container.querySelector('.reference');
container.insertBefore(newElement, referenceElement);

// Modern insertion methods
const html = '<div class="new">New Content</div>';

// insertAdjacentHTML
container.insertAdjacentHTML('beforebegin', html); // Before element
container.insertAdjacentHTML('afterbegin', html); // First child
container.insertAdjacentHTML('beforeend', html); // Last child
container.insertAdjacentHTML('afterend', html); // After element

// insertAdjacentElement
const adjacentElement = document.createElement('span');
container.insertAdjacentElement('afterbegin', adjacentElement);

// insertAdjacentText
container.insertAdjacentText('beforeend', 'Plain text');

// prepend and append (modern methods)
container.prepend(newElement); // Add as first child
container.append(newElement); // Add as last child

// before and after
element.before(newElement); // Insert before element
element.after(newElement); // Insert after element

// Removing elements
element.remove(); // Modern way

// Old way (still works)
element.parentNode.removeChild(element);

// Remove all children
while (container.firstChild) {
  container.removeChild(container.firstChild);
}
// Or
container.innerHTML = '';

// Replace elements
const oldElement = document.querySelector('.old');
const replacement = document.createElement('div');
oldElement.replaceWith(replacement);

// Old way
oldElement.parentNode.replaceChild(replacement, oldElement);

Event Handling

Adding Event Listeners

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

// addEventListener (recommended)
button.addEventListener('click', function (event) {
  console.log('Button clicked!');
  console.log(event.target); // Element that triggered the event
});

// Arrow function
button.addEventListener('click', (event) => {
  console.log('Clicked with arrow function');
});

// Named function (can be removed later)
function handleClick(event) {
  console.log('Named function handler');
}
button.addEventListener('click', handleClick);

// With options
button.addEventListener('click', handleClick, {
  once: true, // Remove after first trigger
  passive: true, // Won't call preventDefault()
  capture: false, // Use bubbling phase (default)
});

// Multiple event types
['click', 'touchstart'].forEach((eventType) => {
  button.addEventListener(eventType, handleClick);
});

Event Object

element.addEventListener('click', function (event) {
  // Event properties
  console.log(event.type); // 'click'
  console.log(event.target); // Element that triggered event
  console.log(event.currentTarget); // Element with listener attached
  console.log(event.timeStamp); // When event occurred

  // Mouse event properties
  console.log(event.clientX, event.clientY); // Mouse position relative to viewport
  console.log(event.pageX, event.pageY); // Mouse position relative to page
  console.log(event.screenX, event.screenY); // Mouse position relative to screen

  // Keyboard event properties
  console.log(event.key); // 'Enter', 'a', 'A', etc.
  console.log(event.code); // 'Enter', 'KeyA', etc.
  console.log(event.keyCode); // Deprecated numeric code
  console.log(event.altKey); // Boolean
  console.log(event.ctrlKey); // Boolean
  console.log(event.shiftKey); // Boolean
  console.log(event.metaKey); // Boolean (Cmd on Mac)

  // Prevent default behavior
  event.preventDefault();

  // Stop event propagation
  event.stopPropagation();

  // Stop immediate propagation
  event.stopImmediatePropagation();
});

Event Delegation

// Instead of adding listeners to many elements
const buttons = document.querySelectorAll('.btn');
buttons.forEach((btn) => {
  btn.addEventListener('click', handleClick);
});

// Use event delegation on parent
document
  .querySelector('.button-container')
  .addEventListener('click', function (event) {
    // Check if clicked element matches selector
    if (event.target.matches('.btn')) {
      handleClick(event);
    }

    // Or use closest for nested elements
    const button = event.target.closest('.btn');
    if (button) {
      handleClick(event);
    }
  });

// Practical example: Dynamic list
const list = document.querySelector('ul');

list.addEventListener('click', function (event) {
  const li = event.target.closest('li');
  if (!li) return;

  if (event.target.matches('.delete-btn')) {
    li.remove();
  } else if (event.target.matches('.edit-btn')) {
    editItem(li);
  } else {
    toggleComplete(li);
  }
});

Common Events

// Mouse events
element.addEventListener('click', handler);
element.addEventListener('dblclick', handler);
element.addEventListener('mousedown', handler);
element.addEventListener('mouseup', handler);
element.addEventListener('mouseover', handler); // Bubbles
element.addEventListener('mouseout', handler); // Bubbles
element.addEventListener('mouseenter', handler); // Doesn't bubble
element.addEventListener('mouseleave', handler); // Doesn't bubble
element.addEventListener('mousemove', handler);
element.addEventListener('contextmenu', handler); // Right-click

// Keyboard events
document.addEventListener('keydown', handler);
document.addEventListener('keyup', handler);
document.addEventListener('keypress', handler); // Deprecated

// Form events
form.addEventListener('submit', handler);
input.addEventListener('input', handler); // Value changes
input.addEventListener('change', handler); // Value committed
input.addEventListener('focus', handler);
input.addEventListener('blur', handler);
select.addEventListener('change', handler);

// Window/Document events
window.addEventListener('load', handler); // Everything loaded
window.addEventListener('DOMContentLoaded', handler); // DOM ready
window.addEventListener('resize', handler);
window.addEventListener('scroll', handler);
window.addEventListener('beforeunload', handler);

// Touch events (mobile)
element.addEventListener('touchstart', handler);
element.addEventListener('touchmove', handler);
element.addEventListener('touchend', handler);
element.addEventListener('touchcancel', handler);

// Drag events
element.addEventListener('dragstart', handler);
element.addEventListener('drag', handler);
element.addEventListener('dragend', handler);
element.addEventListener('dragover', handler);
element.addEventListener('drop', handler);

Practical Examples

Dynamic Form Validation

const form = document.querySelector('#signup-form');
const emailInput = document.querySelector('#email');
const passwordInput = document.querySelector('#password');
const submitButton = document.querySelector('#submit');

// Real-time validation
emailInput.addEventListener('input', function () {
  const email = this.value;
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  if (isValid) {
    this.classList.remove('error');
    this.classList.add('valid');
  } else {
    this.classList.remove('valid');
    this.classList.add('error');
  }

  updateSubmitButton();
});

passwordInput.addEventListener('input', function () {
  const password = this.value;
  const strength = checkPasswordStrength(password);

  this.dataset.strength = strength;
  updatePasswordIndicator(strength);
  updateSubmitButton();
});

function updateSubmitButton() {
  const isEmailValid = emailInput.classList.contains('valid');
  const isPasswordValid = passwordInput.value.length >= 8;

  submitButton.disabled = !(isEmailValid && isPasswordValid);
}

form.addEventListener('submit', function (event) {
  event.preventDefault();

  const formData = new FormData(this);
  const data = Object.fromEntries(formData);

  console.log('Form submitted:', data);
});

Interactive Todo List

class TodoList {
  constructor(container) {
    this.container = container;
    this.todos = [];
    this.init();
  }

  init() {
    this.container.innerHTML = `
      <div class="todo-input">
        <input type="text" placeholder="Add new todo..." />
        <button type="button">Add</button>
      </div>
      <ul class="todo-list"></ul>
      <div class="todo-filters">
        <button data-filter="all" class="active">All</button>
        <button data-filter="active">Active</button>
        <button data-filter="completed">Completed</button>
      </div>
    `;

    this.bindEvents();
  }

  bindEvents() {
    const input = this.container.querySelector('input');
    const addButton = this.container.querySelector('button');
    const list = this.container.querySelector('.todo-list');
    const filters = this.container.querySelector('.todo-filters');

    // Add todo
    addButton.addEventListener('click', () => {
      if (input.value.trim()) {
        this.addTodo(input.value);
        input.value = '';
      }
    });

    input.addEventListener('keypress', (e) => {
      if (e.key === 'Enter' && input.value.trim()) {
        this.addTodo(input.value);
        input.value = '';
      }
    });

    // Todo interactions
    list.addEventListener('click', (e) => {
      const li = e.target.closest('li');
      if (!li) return;

      const id = parseInt(li.dataset.id);

      if (e.target.matches('.toggle')) {
        this.toggleTodo(id);
      } else if (e.target.matches('.delete')) {
        this.deleteTodo(id);
      }
    });

    // Filters
    filters.addEventListener('click', (e) => {
      if (e.target.matches('button')) {
        filters.querySelectorAll('button').forEach((btn) => {
          btn.classList.remove('active');
        });
        e.target.classList.add('active');
        this.filterTodos(e.target.dataset.filter);
      }
    });
  }

  addTodo(text) {
    const todo = {
      id: Date.now(),
      text,
      completed: false,
    };

    this.todos.push(todo);
    this.render();
  }

  toggleTodo(id) {
    const todo = this.todos.find((t) => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.render();
    }
  }

  deleteTodo(id) {
    this.todos = this.todos.filter((t) => t.id !== id);
    this.render();
  }

  filterTodos(filter) {
    const items = this.container.querySelectorAll('.todo-list li');

    items.forEach((item) => {
      const isCompleted = item.classList.contains('completed');

      switch (filter) {
        case 'active':
          item.style.display = isCompleted ? 'none' : '';
          break;
        case 'completed':
          item.style.display = isCompleted ? '' : 'none';
          break;
        default:
          item.style.display = '';
      }
    });
  }

  render() {
    const list = this.container.querySelector('.todo-list');

    list.innerHTML = this.todos
      .map(
        (todo) => `
      <li data-id="${todo.id}" class="${todo.completed ? 'completed' : ''}">
        <input type="checkbox" class="toggle" ${todo.completed ? 'checked' : ''} />
        <span>${todo.text}</span>
        <button class="delete">×</button>
      </li>
    `
      )
      .join('');
  }
}

// Initialize
const todoList = new TodoList(document.querySelector('#todo-container'));

Image Gallery

class ImageGallery {
  constructor(container, images) {
    this.container = container;
    this.images = images;
    this.currentIndex = 0;
    this.init();
  }

  init() {
    this.render();
    this.bindEvents();
  }

  render() {
    this.container.innerHTML = `
      <div class="gallery-main">
        <img src="${this.images[this.currentIndex].url}" 
             alt="${this.images[this.currentIndex].caption}" />
        <div class="gallery-caption">${this.images[this.currentIndex].caption}</div>
        <button class="gallery-prev">‹</button>
        <button class="gallery-next">›</button>
      </div>
      <div class="gallery-thumbnails">
        ${this.images
          .map(
            (img, index) => `
          <img src="${img.thumbnail}" 
               alt="${img.caption}"
               data-index="${index}"
               class="${index === this.currentIndex ? 'active' : ''}" />
        `
          )
          .join('')}
      </div>
    `;
  }

  bindEvents() {
    const prevBtn = this.container.querySelector('.gallery-prev');
    const nextBtn = this.container.querySelector('.gallery-next');
    const thumbnails = this.container.querySelector('.gallery-thumbnails');

    prevBtn.addEventListener('click', () => this.navigate(-1));
    nextBtn.addEventListener('click', () => this.navigate(1));

    thumbnails.addEventListener('click', (e) => {
      if (e.target.matches('img')) {
        this.goToImage(parseInt(e.target.dataset.index));
      }
    });

    // Keyboard navigation
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowLeft') this.navigate(-1);
      if (e.key === 'ArrowRight') this.navigate(1);
    });
  }

  navigate(direction) {
    this.currentIndex += direction;

    if (this.currentIndex < 0) {
      this.currentIndex = this.images.length - 1;
    } else if (this.currentIndex >= this.images.length) {
      this.currentIndex = 0;
    }

    this.updateDisplay();
  }

  goToImage(index) {
    this.currentIndex = index;
    this.updateDisplay();
  }

  updateDisplay() {
    const mainImg = this.container.querySelector('.gallery-main img');
    const caption = this.container.querySelector('.gallery-caption');
    const thumbnails = this.container.querySelectorAll(
      '.gallery-thumbnails img'
    );

    mainImg.src = this.images[this.currentIndex].url;
    mainImg.alt = this.images[this.currentIndex].caption;
    caption.textContent = this.images[this.currentIndex].caption;

    thumbnails.forEach((thumb, index) => {
      thumb.classList.toggle('active', index === this.currentIndex);
    });
  }
}

Performance Considerations

// Batch DOM updates
function updateMultipleElements(updates) {
  // Bad: Causes multiple reflows
  updates.forEach((update) => {
    const element = document.querySelector(update.selector);
    element.style.width = update.width;
    element.style.height = update.height;
  });

  // Good: Minimize reflows
  const fragment = document.createDocumentFragment();
  updates.forEach((update) => {
    const element = document.querySelector(update.selector);
    element.style.cssText = `width: ${update.width}; height: ${update.height}`;
  });
}

// Debounce expensive operations
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

window.addEventListener(
  'resize',
  debounce(() => {
    // Expensive resize operations
  }, 250)
);

// Use requestAnimationFrame for animations
function smoothScroll(target, duration) {
  const targetPosition = target.getBoundingClientRect().top;
  const startPosition = window.pageYOffset;
  let startTime = null;

  function animation(currentTime) {
    if (startTime === null) startTime = currentTime;
    const timeElapsed = currentTime - startTime;
    const run = ease(timeElapsed, startPosition, targetPosition, duration);
    window.scrollTo(0, run);
    if (timeElapsed < duration) requestAnimationFrame(animation);
  }

  function ease(t, b, c, d) {
    t /= d / 2;
    if (t < 1) return (c / 2) * t * t + b;
    t--;
    return (-c / 2) * (t * (t - 2) - 1) + b;
  }

  requestAnimationFrame(animation);
}

Best Practices

  1. Cache DOM queries

    // Bad
    for (let i = 0; i < 100; i++) {
      document.querySelector('.counter').textContent = i;
    }
    
    // Good
    const counter = document.querySelector('.counter');
    for (let i = 0; i < 100; i++) {
      counter.textContent = i;
    }
    
  2. Use event delegation

    // Instead of many listeners
    // Good for dynamic content
    document.addEventListener('click', (e) => {
      if (e.target.matches('.button')) {
        handleButtonClick(e);
      }
    });
    
  3. Avoid inline styles when possible

    // Use classes instead
    element.classList.add('highlighted');
    // Rather than
    element.style.backgroundColor = 'yellow';
    
  4. Clean up event listeners

    const controller = new AbortController();
    
    element.addEventListener('click', handler, {
      signal: controller.signal,
    });
    
    // Later, remove all listeners
    controller.abort();
    

Conclusion

DOM manipulation is fundamental to creating interactive web applications:

  • Selection methods for finding elements
  • Creation and modification of elements
  • Event handling for interactivity
  • Performance optimization for smooth UIs
  • Best practices for maintainable code

Key takeaways:

  • Use modern methods like querySelector and classList
  • Leverage event delegation for dynamic content
  • Batch DOM updates for better performance
  • Clean up event listeners to prevent memory leaks
  • Consider using frameworks for complex applications

Master DOM manipulation to build dynamic, interactive web experiences!