JavaScript DOM Manipulation: The Complete Guide for 2026

February 12, 2026 · 30 min read

Every interactive web page depends on the Document Object Model. When you click a button and a modal appears, when you type in a search box and results filter in real time, when a page smoothly loads more content as you scroll — all of that is DOM manipulation. It is the bridge between your JavaScript code and what the user sees and interacts with on screen.

Despite the rise of frameworks like React and Vue, understanding the DOM API directly remains essential. Frameworks abstract the DOM, but they do not replace it. When you need to build a custom component, debug a rendering issue, optimize performance, or work outside a framework context, raw DOM knowledge is what separates a confident developer from one who is guessing.

This guide covers everything you need to know about DOM manipulation in 2026: selecting elements, creating and removing nodes, handling events, traversing the tree, observing changes, and writing performant code. Every section includes practical code examples you can use immediately.

⚙ Try it live: Test any code example in this guide with our JavaScript Runner or preview HTML output with the HTML Preview tool.

Table of Contents

  1. What is the DOM?
  2. Selecting Elements
  3. Creating and Inserting Elements
  4. Modifying Elements
  5. Removing Elements
  6. DOM Traversal
  7. Event Handling
  8. Form Handling with the DOM
  9. DOM Mutation Observer
  10. Performance Best Practices
  11. Modern DOM APIs
  12. Common Patterns and Recipes
  13. Frequently Asked Questions

1. What is the DOM?

The Document Object Model is a programming interface for HTML documents. When a browser loads an HTML page, it parses the markup and builds a tree of objects — the DOM tree. Each HTML element becomes a node in this tree, and JavaScript can read, modify, add, or remove any node at any time.

<!-- This HTML... -->
<div id="app">
  <h1>Hello</h1>
  <p>Welcome to the <strong>DOM</strong>.</p>
</div>

// ...becomes this tree of objects:
// document
//   └── html
//       ├── head
//       └── body
//           └── div#app
//               ├── h1
//               │   └── "Hello" (text node)
//               └── p
//                   ├── "Welcome to the " (text node)
//                   ├── strong
//                   │   └── "DOM" (text node)
//                   └── "." (text node)

The key insight is that the DOM is live. When you change a node with JavaScript, the browser immediately updates the page. This makes the DOM the foundation of all dynamic web behavior.

Node Types

// The most common node types:
// 1 (Element) — div, span, p, etc.
// 3 (Text)    — text content inside elements
// 9 (Document) — the document itself

const div = document.createElement('div');
console.log(div.nodeType);  // 1 (Node.ELEMENT_NODE)
console.log(div.nodeName);  // "DIV"

// Check node type
if (node.nodeType === Node.ELEMENT_NODE) { /* element */ }
if (node.nodeType === Node.TEXT_NODE) { /* text */ }

2. Selecting Elements

Before you can manipulate an element, you need to select it. Modern JavaScript provides several methods, but querySelector and querySelectorAll are the workhorses you will use most.

querySelector and querySelectorAll

// querySelector returns the FIRST matching element (or null)
const header = document.querySelector('h1');
const nav = document.querySelector('.nav-links');
const email = document.querySelector('#email');
const submitBtn = document.querySelector('form button[type="submit"]');

// querySelectorAll returns ALL matching elements as a NodeList
const items = document.querySelectorAll('.list-item');
const links = document.querySelectorAll('nav a');
const inputs = document.querySelectorAll('input[type="text"]');

// NodeList is iterable with forEach, for...of, and spread
items.forEach(item => console.log(item.textContent));

for (const link of links) {
  link.style.color = 'blue';
}

// Convert to array for full array methods
const inputArray = [...inputs];
const values = inputArray.map(input => input.value);

// Any valid CSS selector works
document.querySelector('div > p:first-child');
document.querySelector('[data-role="admin"]');
document.querySelector('.sidebar .widget:nth-child(3)');
document.querySelector('input:not([disabled])');

getElementById, getElementsByClassName, getElementsByTagName

// getElementById — fastest single-element selector
const app = document.getElementById('app');
// Note: no # prefix, just the id string

// getElementsByClassName — returns a live HTMLCollection
const cards = document.getElementsByClassName('card');
// Live: if you add a .card element, it automatically appears in the collection

// getElementsByTagName — all elements of a tag
const paragraphs = document.getElementsByTagName('p');
const allElements = document.getElementsByTagName('*');

// HTMLCollection vs NodeList:
// - HTMLCollection (getElementsBy*) is LIVE — updates automatically
// - NodeList (querySelectorAll) is STATIC — a snapshot at query time
// - HTMLCollection has NO forEach — convert to array first

const liveCards = document.getElementsByClassName('card');
console.log(liveCards.length); // 3

document.body.appendChild(document.createElement('div')).className = 'card';
console.log(liveCards.length); // 4 — it updated automatically!

// Convert HTMLCollection to array
const cardArray = [...cards];
const cardArray2 = Array.from(cards);

Closest, Matches, and Contains

// closest() — find the nearest ancestor matching a selector
const button = document.querySelector('.delete-btn');
const card = button.closest('.card');       // nearest .card ancestor
const missing = button.closest('.nonexistent'); // null

// matches() — check if an element matches a selector
el.matches('.item.active');       // true if it has both classes

// contains() — check if one element is inside another
parent.contains(child);           // true if child is inside parent

3. Creating and Inserting Elements

Creating new elements and inserting them into the DOM is fundamental to building dynamic interfaces. There are multiple approaches, each with its own strengths.

createElement and appendChild

// Create a new element
const card = document.createElement('div');
card.className = 'card';
card.id = 'card-1';

const title = document.createElement('h2');
title.textContent = 'New Card';

const body = document.createElement('p');
body.textContent = 'This card was created with JavaScript.';

// Build the tree and insert into the page
card.appendChild(title);
card.appendChild(body);
document.querySelector('.container').appendChild(card);

// append() accepts multiple nodes at once
function createUserCard(user) {
  const card = document.createElement('div');
  card.className = 'user-card';
  const name = document.createElement('h3');
  name.textContent = user.name;
  const email = document.createElement('p');
  email.textContent = user.email;
  card.append(name, email);
  return card;
}

insertAdjacentHTML

// Insert HTML string at a specific position
// Positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
const list = document.querySelector('.list');

list.insertAdjacentHTML('afterbegin', '<li>First</li>');  // first child
list.insertAdjacentHTML('beforeend', '<li>Last</li>');    // last child
list.insertAdjacentHTML('beforebegin', '<h3>Title</h3>'); // before element
list.insertAdjacentHTML('afterend', '<p>Footer</p>');    // after element

// Visual guide:
// <!-- beforebegin -->
// <ul class="list">
//   <!-- afterbegin -->
//   <li>existing item</li>
//   <!-- beforeend -->
// </ul>
// <!-- afterend -->

prepend, append, before, after, replaceWith

// Modern insertion methods (accept multiple nodes AND strings)
const container = document.querySelector('.container');

container.prepend('Text ', document.createElement('hr')); // beginning (inside)
container.append(document.createElement('div'), ' text'); // end (inside)
target.before(document.createElement('p'));  // before the element (outside)
target.after('Some text after');             // after the element (outside)
oldCard.replaceWith(newCard);                // replace the element entirely

// Clone an element
const clone = template.cloneNode(true);  // true = deep clone (with children)
clone.querySelector('h2').textContent = 'Cloned Card';
container.appendChild(clone);

4. Modifying Elements

Once you have a reference to an element, you can change its content, attributes, styles, and classes.

Text Content and HTML

const el = document.querySelector('.message');

// textContent — get/set plain text (safe, no HTML parsing)
el.textContent = 'Hello, world!';

// innerHTML — get/set HTML content (parses HTML tags)
el.innerHTML = '<strong>Bold</strong> and <em>italic</em>';
// WARNING: never use innerHTML with user input (XSS risk!)

// innerText — like textContent but respects CSS visibility (triggers reflow)
// outerHTML — includes the element itself in the HTML string

Attributes

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

// Standard attributes — use properties directly
link.href = 'https://example.com';
link.target = '_blank';

// getAttribute/setAttribute — for any attribute
link.setAttribute('aria-label', 'Visit Example');
link.hasAttribute('target');   // true
link.removeAttribute('target');

// data-* attributes — use the dataset property
const card = document.querySelector('.card');
card.dataset.userId = '42';    // sets data-user-id="42"
card.dataset.role = 'admin';   // sets data-role="admin"
console.log(card.dataset.userId); // "42"
delete card.dataset.role;      // removes data-role
// Note: data-user-id in HTML becomes dataset.userId in JS (camelCase)

classList

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

el.classList.add('active');              // add one class
el.classList.add('highlight', 'visible'); // add multiple
el.classList.remove('hidden');            // remove
el.classList.toggle('active');            // add if missing, remove if present
el.classList.toggle('dark', isDarkMode);  // conditional: true=add, false=remove
el.classList.contains('active');          // check if exists
el.classList.replace('old', 'new');       // replace one class with another

Inline Styles

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

// Set individual styles (camelCase property names)
el.style.backgroundColor = '#3b82f6';
el.style.padding = '1rem';
el.style.borderRadius = '8px';

// Set multiple styles at once
el.style.cssText = 'background: red; padding: 1rem; display: flex;';

// Read computed styles (including inherited and stylesheet values)
const computed = getComputedStyle(el);
console.log(computed.fontSize);  // "16px"

// CSS custom properties
el.style.setProperty('--card-color', '#3b82f6');
getComputedStyle(el).getPropertyValue('--card-color');

5. Removing Elements

// remove() — the modern way
document.querySelector('.card').remove();

// removeChild() — traditional (needs parent reference)
list.removeChild(list.querySelector('li'));

// Remove all children
container.replaceChildren();         // modern, clean
container.innerHTML = '';            // fast but loses event listeners

// Remove elements matching a condition
document.querySelectorAll('.item.completed').forEach(el => el.remove());

// Remove after animation
toast.classList.add('fade-out');
toast.addEventListener('animationend', () => toast.remove());

6. DOM Traversal

DOM traversal means navigating between nodes — moving from parent to child, between siblings, or upward to ancestors. There are two sets of properties: one that includes all nodes (including text and comments), and one that includes only element nodes.

// Given this HTML:
// <ul id="menu">
//   <li>Home</li>
//   <li class="active">About</li>
//   <li>Contact</li>
// </ul>

const menu = document.querySelector('#menu');
const active = document.querySelector('.active');

// ELEMENT-ONLY traversal (what you want 99% of the time)
menu.children;             // HTMLCollection of li elements
menu.firstElementChild;    // first li (Home)
menu.lastElementChild;     // last li (Contact)
menu.childElementCount;    // 3

active.parentElement;      // the ul#menu
active.nextElementSibling; // li (Contact)
active.previousElementSibling; // li (Home)

// ALL-NODE traversal (includes text nodes, whitespace, comments)
// childNodes, firstChild, lastChild, nextSibling, previousSibling
// Use these only when you need text nodes; prefer element traversal above

// Useful helpers
const getSiblings = el =>
  [...el.parentElement.children].filter(child => child !== el);

function walkTree(root, callback) {
  callback(root);
  for (const child of root.children) walkTree(child, callback);
}

7. Event Handling

Events are the mechanism that makes web pages interactive. Every user action — clicking, typing, scrolling, hovering — fires an event that you can listen for and respond to.

addEventListener

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

// Basic event listener
button.addEventListener('click', function(event) {
  console.log('Button clicked!');
  console.log('Target:', event.target);
});

// Arrow function (common)
button.addEventListener('click', (e) => {
  e.preventDefault(); // prevent default browser behavior
  console.log('Clicked');
});

// Named function (removable)
function handleClick(e) {
  console.log('Clicked at', e.clientX, e.clientY);
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // exact same reference required

// Options object
button.addEventListener('click', handleClick, {
  once: true,    // automatically removes after first call
  passive: true, // promises not to call preventDefault (performance)
  capture: true  // fires during capture phase instead of bubble phase
});

// Common event types: click, dblclick, mouseenter, mouseleave,
// keydown, keyup, input, change, submit, focus, blur, scroll

The Event Object

document.addEventListener('click', (e) => {
  e.target;          // the element that was clicked
  e.currentTarget;   // the element the listener is attached to
  e.clientX, e.clientY; // mouse position relative to viewport
  e.shiftKey, e.ctrlKey, e.altKey, e.metaKey; // modifier keys
  e.preventDefault();    // stop default behavior
  e.stopPropagation();   // stop bubbling to parent elements
});

document.addEventListener('keydown', (e) => {
  e.key;    // "Enter", "Escape", "a", "ArrowUp"
  e.code;   // "KeyA" (physical key, layout-independent)
  if (e.key === 'Escape') closeModal();
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    saveDocument();
  }
});

Event Delegation

// BAD: individual listeners on 100 items
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', handleItemClick);
});
// Problems: memory-heavy, doesn't work for dynamically added items

// GOOD: one listener on the parent
const list = document.querySelector('.item-list');
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (!item) return; // click was not on an item
  if (!list.contains(item)) return; // safety check

  console.log('Clicked item:', item.dataset.id);
  item.classList.toggle('selected');
});

// Works for lists, tables, grids, and any dynamically generated content

8. Form Handling with the DOM

// Access form elements
const form = document.querySelector('#signup-form');

// Listen for form submission
form.addEventListener('submit', (e) => {
  e.preventDefault(); // prevent page reload

  // Access individual fields
  const name = form.querySelector('#name').value;
  const email = form.querySelector('#email').value;

  // Or use FormData for all fields at once
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  // { name: "Alice", email: "alice@example.com", role: "developer" }

  // Validate
  if (!name.trim()) {
    showError('Name is required');
    return;
  }

  // Submit
  fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
});

// Real-time validation
emailInput.addEventListener('input', (e) => {
  const isValid = e.target.value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  e.target.classList.toggle('invalid', !isValid);
});

// Reset form and access different input types
form.reset();
document.querySelector('#country').value;             // select value
document.querySelector('#agree').checked;             // checkbox
[...document.querySelectorAll('[name="plan"]')]
  .find(r => r.checked)?.value;                       // radio value

9. DOM Mutation Observer

MutationObserver watches for changes to the DOM and calls your callback when nodes are added, removed, or modified. It replaced the deprecated Mutation Events API and is far more performant.

// Watch for child elements being added or removed
const container = document.querySelector('#content');

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log('Added:', node.tagName, node.className);
        }
      });
      mutation.removedNodes.forEach(node => {
        if (node.nodeType === Node.ELEMENT_NODE) {
          console.log('Removed:', node.tagName);
        }
      });
    }
  }
});

observer.observe(container, {
  childList: true,  // watch for added/removed children
  subtree: true     // watch all descendants, not just direct children
});

// Watch for attribute changes
const badge = document.querySelector('.badge');

const attrObserver = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === 'attributes') {
      console.log(`${mutation.attributeName} changed to`,
        badge.getAttribute(mutation.attributeName));
    }
  }
});

attrObserver.observe(badge, {
  attributes: true,
  attributeFilter: ['class', 'data-count'] // only these attributes
});

// Stop observing when done
observer.disconnect();

10. Performance Best Practices

DOM manipulation is one of the most expensive operations in the browser. Every change can trigger layout recalculation (reflow), style computation, and repainting. Writing performant DOM code means minimizing these operations.

DocumentFragment

// BAD: 1000 individual DOM insertions (1000 reflows)
const list = document.querySelector('.list');
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  list.appendChild(li); // triggers reflow each time
}

// GOOD: build in a DocumentFragment, insert once (1 reflow)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li); // no reflow — fragment is off-screen
}
list.appendChild(fragment); // single reflow for all 1000 items

Batch DOM Reads and Writes

// BAD: interleaving reads and writes forces multiple reflows
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
  const height = box.offsetHeight; // READ — forces reflow
  box.style.height = height * 2 + 'px'; // WRITE — invalidates layout
  // Next iteration: READ forces another reflow because layout is dirty
});

// GOOD: batch all reads first, then all writes
const heights = [];
boxes.forEach(box => {
  heights.push(box.offsetHeight); // all READs together
});
boxes.forEach((box, i) => {
  box.style.height = heights[i] * 2 + 'px'; // all WRITEs together
});
// Only one reflow for the entire batch

requestAnimationFrame

// Use requestAnimationFrame for visual updates
function animateProgress(target) {
  let current = 0;

  function step() {
    current += 2;
    progressBar.style.width = current + '%';

    if (current < target) {
      requestAnimationFrame(step); // syncs with display refresh
    }
  }

  requestAnimationFrame(step);
}

// Debounce scroll/resize handlers with rAF
let ticking = false;
window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateHeaderOnScroll();
      ticking = false;
    });
    ticking = true;
  }
});

Cache DOM References

// BAD: querying the DOM inside a loop
function updateItems(data) {
  data.forEach(item => {
    // querySelector runs every iteration
    document.querySelector('.container').appendChild(/* ... */);
  });
}

// GOOD: cache the reference
function updateItems(data) {
  const container = document.querySelector('.container');
  const fragment = document.createDocumentFragment();

  data.forEach(item => {
    const el = document.createElement('div');
    el.textContent = item.name;
    fragment.appendChild(el);
  });

  container.appendChild(fragment);
}

11. Modern DOM APIs

Intersection Observer

Intersection Observer detects when elements enter or leave the viewport. It replaces scroll event listeners for lazy loading, infinite scroll, and scroll-triggered animations.

// Lazy load images when they enter the viewport
const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;     // load the real image
      img.removeAttribute('data-src');
      imageObserver.unobserve(img);  // stop watching this image
    }
  });
}, {
  rootMargin: '200px' // start loading 200px before visible
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

// Scroll-triggered animations
const animObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    entry.target.classList.toggle('visible', entry.isIntersecting);
  });
}, {
  threshold: 0.1 // trigger when 10% visible
});

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  animObserver.observe(el);
});

// Infinite scroll
const sentinel = document.querySelector('#scroll-sentinel');
const scrollObserver = new IntersectionObserver(async (entries) => {
  if (entries[0].isIntersecting) {
    const newItems = await fetchMoreItems();
    appendItems(newItems);
  }
});
scrollObserver.observe(sentinel);

Resize Observer

// Watch an element's size changes (not window resize — element resize)
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`Element resized: ${width}x${height}`);

    // Adjust layout based on element size (container queries in JS)
    if (width < 400) {
      entry.target.classList.add('compact');
    } else {
      entry.target.classList.remove('compact');
    }
  }
});

const card = document.querySelector('.responsive-card');
resizeObserver.observe(card);

// Stop observing
resizeObserver.unobserve(card);
resizeObserver.disconnect(); // stop all observations

AbortController for Event Cleanup

// Remove multiple event listeners at once with AbortController
const controller = new AbortController();
const { signal } = controller;

document.addEventListener('click', handleClick, { signal });
document.addEventListener('keydown', handleKeydown, { signal });
window.addEventListener('resize', handleResize, { signal });

// Later: remove ALL listeners with one call
controller.abort();

// Great for component lifecycle
function initWidget(container) {
  const controller = new AbortController();
  const { signal } = controller;
  container.addEventListener('click', onClick, { signal });
  document.addEventListener('keydown', onKey, { signal });
  return () => controller.abort(); // cleanup function
}

12. Common Patterns and Recipes

Modal Dialog

function createModal(content) {
  const overlay = document.createElement('div');
  overlay.className = 'modal-overlay';
  const modal = document.createElement('div');
  modal.className = 'modal';
  modal.innerHTML = content;
  overlay.appendChild(modal);
  document.body.appendChild(overlay);

  const close = () => overlay.remove();
  modal.querySelector('.modal-close')?.addEventListener('click', close);
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) close();
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') close();
  }, { once: true });
  return { close };
}

Debounced Search Input

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const searchInput = document.querySelector('#search');
const results = document.querySelector('#results');

const search = debounce(async (query) => {
  if (!query.trim()) { results.innerHTML = ''; return; }
  const data = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
    .then(r => r.json());
  results.innerHTML = data.map(item =>
    `<div class="result"><h3>${item.title}</h3></div>`
  ).join('');
}, 300);

searchInput.addEventListener('input', (e) => search(e.target.value));

Dark Mode Toggle

const isDark = localStorage.getItem('theme') === 'dark' ||
  (!localStorage.getItem('theme') &&
   window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);

document.querySelector('#dark-mode-toggle').addEventListener('click', () => {
  const dark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', dark ? 'dark' : 'light');
});

Copy to Clipboard

async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('Copied!');
  } catch {
    // Fallback for older browsers
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    textarea.remove();
  }
}

Frequently Asked Questions

What is the difference between querySelector and getElementById?

getElementById selects a single element by its id attribute and is slightly faster. querySelector accepts any CSS selector (classes, attributes, pseudo-selectors, combinators) and is far more flexible. In practice, querySelector is preferred for most use cases because it handles any selector pattern, while getElementById is only useful when you have an id. Performance differences are negligible for typical applications.

What is event delegation and why should I use it?

Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on each child. When an event fires on a child, it bubbles up to the parent where your handler checks event.target to determine which child was clicked. This is more memory-efficient, works automatically with dynamically added elements, and simplifies code when you have many similar elements like list items, table rows, or buttons.

Should I use innerHTML or createElement to add elements to the DOM?

Use createElement when you need fine-grained control, when working with user input (innerHTML can introduce XSS vulnerabilities), or when you need to attach event listeners to the new elements. Use innerHTML for simple static HTML insertions where security is not a concern. For the best of both worlds, use template literals with insertAdjacentHTML, which lets you insert HTML strings at specific positions without overwriting existing content.

How do I improve DOM manipulation performance?

Batch your DOM updates to minimize reflows and repaints. Use DocumentFragment to build complex structures off-screen before inserting them. Cache DOM references instead of querying repeatedly. Use requestAnimationFrame for visual updates. Prefer classList over className for toggling classes. Use event delegation instead of individual listeners. For large lists, consider virtual scrolling with Intersection Observer to render only visible items.

What are Mutation Observer and Intersection Observer used for?

MutationObserver watches for changes to the DOM tree, such as added or removed nodes, attribute changes, and text content modifications. It is useful for building plugins, auto-saving content, and reacting to third-party DOM changes. IntersectionObserver detects when an element enters or exits the viewport, making it ideal for lazy loading images, infinite scroll, scroll-triggered animations, and tracking which content the user has actually seen.

Conclusion

The DOM API is the foundation of everything interactive in a web browser. Frameworks abstract it, but they all rely on the same underlying methods. When you understand the DOM deeply, you write better framework code, debug faster, and build things no framework covers out of the box.

Start with querySelector, textContent, classList, and addEventListener with event delegation. Then move on to DocumentFragment, batched updates, and modern observers. The best way to learn is to build things without a framework — each project reinforces the patterns in this guide.

⚙ Practice: Try every example in this guide using our JavaScript Runner, or preview HTML output with the HTML Preview tool.

Learn More

Related: Check out our JavaScript ES6+ Features Guide for the modern syntax used throughout this article, and our JavaScript Array Methods Guide for working with data after you extract it from the DOM.

Related Resources

JavaScript ES6+ Features Guide
Complete reference for modern JavaScript syntax
JavaScript Array Methods Guide
Every array method with practical examples
JavaScript Runner
Run and test JavaScript code instantly
HTML Preview
Preview HTML, CSS, and JS output in real time
JavaScript Debugging Guide
Debug JavaScript like a pro with DevTools
Web Performance Optimization
Make your web apps fast and responsive
Embed this article

Copy the code below to embed this guide on your site:

<iframe src="https://devtoolbox.dedyn.io/blog/javascript-dom-manipulation-guide" width="100%" height="800" frameborder="0" title="JavaScript DOM Manipulation Guide"></iframe>