DEV Community

Alex Chen
Alex Chen

Posted on

The Complete Guide to JavaScript DOM Manipulation

The Complete Guide to JavaScript DOM Manipulation

DOM manipulation is how you make web pages interactive. Here's everything you need to know.

What is the DOM?

HTML (text) → Browser parses it → DOM (tree of objects) → You manipulate it

DOM = Document Object Model
     = Your HTML as a JavaScript object tree
     = Every HTML tag becomes a JS object you can read/modify
Enter fullscreen mode Exit fullscreen mode
<!-- This HTML: -->
<div id="app">
  <h1 class="title">Hello</h1>
  <p data-id="1">First paragraph</p>
  <p data-id="2">Second paragraph</p>
</div>

<!-- Becomes this DOM tree:
Document
 └── html
      ├── head
      └── body
           └── div#app
                ├── h1.title → "Hello"
                ├── p[data-id="1"] → "First paragraph"
                └── p[data-id="2"] → "Second paragraph"
-->
Enter fullscreen mode Exit fullscreen mode

Selecting Elements

// Single element selectors (returns ONE element or null)
document.getElementById('app');           // By ID (fastest)
document.querySelector('.title');         // First matching CSS selector
document.querySelector('p[data-id="1"]'); // Complex CSS selector

// Multiple element selectors (returns NodeList/HTMLCollection)
document.getElementsByClassName('title'); // Live collection (changes with DOM)
document.getElementsByTagName('p');       // Live collection
document.querySelectorAll('p');          // Static snapshot (doesn't update)

// Best practice: use querySelectorAll for consistency
const items = document.querySelectorAll('.item');
items.forEach(item => console.log(item));

// Note: querySelectorAll returns NodeList, NOT array!
// Convert to array if needed:
Array.from(items);
[...items];  // Spread syntax
Enter fullscreen mode Exit fullscreen mode

Reading & Modifying Content

// Text content (no HTML)
el.textContent = 'Hello';        // Set text
console.log(el.textContent);    // Get text (includes all nested text)

// Inner HTML (parses HTML)
el.innerHTML = '<strong>Bold</strong> text';
// ⚠️ Security risk with user input! Use textContent for user data.

// Outer HTML (includes the element itself)
console.log(el.outerHTML);

// Value (for form elements)
input.value = 'new value';
select.value = 'option2';
textarea.value = 'multiline';

// Attributes
el.getAttribute('href');              // Get attribute
el.setAttribute('href', '/new-url');  // Set attribute
el.removeAttribute('disabled');        // Remove attribute
el.hasAttribute('hidden');            // Check existence

// Properties (preferred over getAttribute for common attributes)
el.id;             // Same as getAttribute('id')
el.className;      // Same as getAttribute('class')
el.href;           // Resolves to full URL (getAttribute returns raw)
el.checked;        // Boolean for checkboxes/radio buttons
el.disabled;       // Boolean
el.value;          // For form elements
Enter fullscreen mode Exit fullscreen mode

Creating & Inserting Elements

// Create elements
const div = document.createElement('div');
const h1 = document.createElement('h1');
const text = document.createTextNode('Hello World');

// Build structure
h1.textContent = 'My Title';
div.appendChild(h1);
div.appendChild(document.createElement('p')).textContent = 'Paragraph';

// Insert into page
document.body.appendChild(div);

// More insertion methods:

// insertBefore — insert before reference node
parent.insertBefore(newEl, existingEl);

// append() — can append multiple items + strings (modern!)
parent.append(child1, child2, 'text string');

// prepend() — insert at beginning of parent
parent.prepend(firstChild);

// before() / after() — insert relative to an element
existingEl.before(newEl);   // Insert right BEFORE existingEl
existingEl.after(newEl);    // Insert right AFTER existingEl

// replaceWith() — replace an element
oldEl.replaceWith(newEl);

// remove() — remove from DOM
el.remove();  // No need to call parent.removeChild(el)

// Practical example: Dynamic list
function renderList(items, containerId) {
  const container = document.getElementById(containerId);
  container.innerHTML = ''; // Clear existing

  items.forEach(item => {
    const li = document.createElement('li');
    li.className = item.active ? 'active' : '';
    li.innerHTML = `<strong>${item.name}</strong>: ${item.value}`;

    const btn = document.createElement('button');
    btn.textContent = 'Delete';
    btn.onclick = () => li.remove();

    li.appendChild(btn);
    container.appendChild(li);
  });
}
Enter fullscreen mode Exit fullscreen mode

Styling Elements

// Individual styles
el.style.color = 'red';
el.style.backgroundColor = '#f0f0f0';
el.style.fontSize = '16px';
el.style.display = 'none';  // Hide
el.style.display = 'block'; // Show

// Better: CSS classes (separation of concerns)
el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('visible');  // Toggle on/off
el.classList.contains('active'); // Check

// Multiple classes at once
el.classList.add('a', 'b', 'c');
el.classList.remove('x', 'y');

// Replace all classes
el.className = 'new-class another-class';

// Get computed style (final applied style)
const styles = window.getComputedStyle(el);
styles.color;           // "rgb(255, 0, 0)"
styles.fontSize;        // "16px"
styles.display;         // "block"
Enter fullscreen mode Exit fullscreen mode

Event Handling

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

// Arrow function (no own `this`)
button.addEventListener('click', (e) => {
  e.preventDefault();  // Prevent default behavior
  e.stopPropagation(); // Stop bubbling to parent
  console.log(e.target);  // Element that triggered event
  e.currentTarget;        // Element listener is attached to
});

// Event delegation (one listener for many children!)
list.addEventListener('click', (e) => {
  if (e.target.matches('li')) {
    console.log('Clicked:', e.target.textContent);
  }
});
// Benefits: Works for dynamically added elements too!

// Common events
element.addEventListener('input', handler);      // Typing in input
element.addEventListener('change', handler);     // Value changed + blur
element.addEventListener('submit', handler);     // Form submitted
element.addEventListener('keydown', handler);    // Key pressed
element.addEventListener('keyup', handler);      // Key released
element.addEventListener('focus', handler);      // Got focus
element.addEventListener('blur', handler);       // Lost focus
window.addEventListener('scroll', handler);       // Scrolling
window.addEventListener('resize', handler);      // Window resized
document.addEventListener('DOMContentLoaded', fn); // HTML parsed, before images

// Remove listener (needs same function reference)
function handleClick() { /* ... */ }
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);

// Once option (auto-removes after firing)
button.addEventListener('click', handler, { once: true });

// Options object (useful options)
button.addEventListener('click', handler, {
  once: false,        // Remove after first trigger?
  capture: false,     // Capture phase vs bubble phase
  passive: true,      // Can't preventDefault (better scroll performance)
});
Enter fullscreen mode Exit fullscreen mode

Traversing the DOM

// Parent/Child relationships
el.parentElement;                    // Direct parent
el.children;                         // Direct children (elements only)
el.childNodes;                       // All child nodes (including text nodes)
el.firstChild;                       // First child node
el.lastChild;                        // Last child node
el.firstElementChild;               // First child ELEMENT
el.lastElementChild;                // Last child ELEMENT
el.childElementCount;               // Number of child elements

// Sibling relationships
el.previousSibling;                  // Previous sibling (any type)
el.nextSibling;                      // Next sibling (any type)
el.previousElementSibling;           // Previous sibling element
el.nextElementSibling;               // Next sibling element

// Closest ancestor matching selector (SUPER useful!)
el.closest('.container');  // Find parent with class "container"
el.closest('[data-type]');  // Find parent with data-type attribute

// Matches selector?
el.matches('.active');       // Does this element have class "active"?
el.contains(otherEl);       // Is otherEl a descendant of this el?
Enter fullscreen mode Exit fullscreen mode

Data Attributes (Custom Data)

<div data-user-id="123" data-role="admin" data-config='{"theme":"dark"}'></div>
Enter fullscreen mode Exit fullscreen mode
// Read data attributes
el.dataset.userId;     // "123"
el.dataset.role;       // "admin"
el.dataset.config;     // '{"theme":"dark"}'
JSON.parse(el.dataset.config); // { theme: 'dark' }

// Write data attributes
el.dataset.newAttr = 'value';
// Adds: data-new-attr="value" (camelCase → kebab-case)

// Check existence
'userId' in el.dataset;  // true

// Use cases:
// - Store element metadata without polluting JS objects
// - Pass configuration to event handlers
// - Store state for UI components
// - Communication between components
Enter fullscreen mode Exit fullscreen mode

Performance Tips

// ❌ Slow: Manipulating DOM in a loop
for (let i = 0; i < 1000; i++) {
  document.body.appendChild(createDiv(i)); // 1000 reflows!
}

// ✅ Fast: DocumentFragment (batch DOM operations)
const fragment = new DocumentFragment();
for (let i = 0; i < 1000; i++) {
  fragment.appendChild(createDiv(i));
}
document.body.appendChild(fragment); // Only 1 reflow!

// ✅ Fast: Clone template
const template = document.querySelector('#row-template');
data.forEach(item => {
  const row = template.content.cloneNode(true);
  row.querySelector('.name').textContent = item.name;
  container.appendChild(row);
});

// ✅ Fast: innerHTML for bulk updates (vs individual createElement calls)
// For large lists, building one big string and setting innerHTML once is faster
// than creating hundreds of individual elements.

// Avoid layout thrashing (reading/writing alternately causes reflows)
// ❌ Bad (forces browser to recalculate layout each iteration):
for (const el of elements) {
  console.log(el.offsetTop);  // Read (forces reflow)
  el.style.height = '100px';  // Write (triggers reflow)
}

// ✅ Good (batch reads, then batch writes):
const heights = Array.from(elements).map(el => el.offsetTop); // All reads
elements.forEach((el, i) => { el.style.height = heights[i] + 'px'; }); // All writes
Enter fullscreen mode Exit fullscreen mode

Quick Reference Card

Task Method
Select by ID getElementById()
Select by CSS querySelector() / querySelectorAll()
Get/set text textContent
Get/set HTML innerHTML
Get/set value .value (forms)
Add/remove/toggle class classList.add/remove/toggle()
Set inline style el.style.property
Create element createElement()
Append child appendChild() / append()
Remove element el.remove()
Insert before/after before() / after()
Add event addEventListener()
Find closest parent closest()
Custom data dataset.*
Batch operations DocumentFragment

What's your favorite DOM manipulation trick?

Follow @armorbreak for more JavaScript content.

Top comments (0)