JavaScript DOM Manipulation

Modify content and styles, insert and remove elements, clone nodes, and update many elements at once.

Beginner 11 min read 11 examples

Modifying Content

Three properties let you read or replace an element's content. Choose based on what you're setting and whether you trust the source.

PropertyGets/SetsParses HTML?Safe for user input?
textContentAll text (including hidden)No - escapes tagsYes
innerTextVisible text onlyNo - escapes tagsYes
innerHTMLHTML markupYes - creates elementsNever with untrusted input
JavaScript
const box = document.querySelector('.box');

// textContent - plain text, HTML tags are escaped (not parsed)
box.textContent = 'Hello World';
// Renders: Hello <strong>World</strong> - shows the tag as text

// innerHTML - parses the string as HTML
box.innerHTML = 'Hello World';
// Renders: Hello **World** - bold "World"

// NEVER do this with user input - XSS vulnerability
const userInput = '<img src=x onerror="alert(1)">';
box.innerHTML = userInput; // DANGEROUS

// Safe alternative for user input
box.textContent = userInput; // Displays as literal text, no script executed

// Reading content
console.log(box.textContent);  // 'Hello World' (no tags, just text)
console.log(box.innerHTML);    // 'Hello <strong>World</strong>'
console.log(box.outerHTML);    // '<div class="box">Hello <strong>World</strong></div>'
JavaScript
// Safe pattern: build with createElement, set text via textContent
function createUserCard(user) {
    const card = document.createElement('div');
    card.className = 'user-card';

    const name = document.createElement('h3');
    name.textContent = user.name; // safe - no XSS possible

    const bio = document.createElement('p');
    bio.textContent = user.bio;   // safe even if bio contains HTML

    card.append(name, bio);
    return card;
}

// Using template literals with innerHTML - only for trusted/controlled data
function renderProductBadge(product) {
    return `
        <span class="badge">${product.category}</span>
        <span class="price">$${product.price}</span>
    `;
    // OK only because product comes from your own API, not user input
}

Classes and Styles

classList is the recommended way to toggle appearance. Direct style manipulation should be a last resort.

JavaScript
const btn = document.querySelector('.btn');

// Add a class
btn.classList.add('active');
btn.classList.add('btn-primary', 'large'); // add multiple at once

// Remove a class
btn.classList.remove('disabled');
btn.classList.remove('btn-primary', 'large');

// Toggle - adds if absent, removes if present; returns true if added
const isActive = btn.classList.toggle('active');
console.log(isActive); // true if 'active' was just added

// Toggle with force: true=always add, false=always remove
btn.classList.toggle('hidden', !isLoggedIn); // hide when not logged in

// Check
console.log(btn.classList.contains('active')); // true or false

// Replace one class with another
btn.classList.replace('btn-primary', 'btn-danger');

// Read all classes
console.log([...btn.classList]); // ['btn', 'active', ...]
console.log(btn.className);      // 'btn active' (space-separated string)
JavaScript
const box = document.querySelector('.box');

// Set inline styles - camelCase property names
box.style.backgroundColor = '#3b82f6';
box.style.padding = '1rem';
box.style.borderRadius = '8px';

// Remove an inline style by setting it to empty string
box.style.backgroundColor = '';

// Read computed style (not inline style) - includes CSS cascade
const computed = getComputedStyle(box);
console.log(computed.backgroundColor); // 'rgb(59, 130, 246)'
console.log(computed.fontSize);        // '16px'

// cssText - replace all inline styles at once
box.style.cssText = 'color: red; font-size: 20px;';

// Prefer classList - define appearance in CSS, toggle classes in JS
// BAD: box.style.display = 'none';
// GOOD: box.classList.add('hidden');  -- .hidden { display: none; } in CSS

// Valid exception: dynamic values you can't express in CSS classes
function setProgress(percent) {
    document.querySelector('.progress-bar').style.width = percent + '%';
}

Attributes and Data

JavaScript
const input = document.querySelector('input');

// setAttribute / getAttribute / removeAttribute / hasAttribute
input.setAttribute('placeholder', 'Enter email...');
input.setAttribute('disabled', '');             // boolean attribute
console.log(input.getAttribute('placeholder')); // 'Enter email...'
console.log(input.hasAttribute('disabled'));    // true
input.removeAttribute('disabled');

// Direct property access (preferred for standard attributes)
input.type        = 'email';
input.placeholder = 'Enter email...';
input.disabled    = false;
// Note: .getAttribute('class') vs .className - both work but differ subtly

// data-* attributes via dataset
const card = document.querySelector('[data-user-id]');
// HTML: <div data-user-id="42" data-role="admin">
console.log(card.dataset.userId); // '42'  (camelCase, always string)
console.log(card.dataset.role);   // 'admin'

card.dataset.userId = '99';       // sets data-user-id="99"
card.dataset.score  = 500;        // sets data-score="500" (coerced to string)
delete card.dataset.score;        // removes data-score attribute

// Parse numbers from dataset
const id = parseInt(card.dataset.userId, 10); // 99 as number

Inserting Elements

Modern DOM APIs give you precise control over where elements are inserted.

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

// append() - add to end, accepts nodes and strings
const li = document.createElement('li');
li.textContent = 'New item';
list.append(li);
list.append('Plain text node'); // string becomes a text node

// prepend() - add to beginning
list.prepend(li);

// before() / after() - insert relative to the element (not inside it)
const heading = document.querySelector('h2');
const note = document.createElement('p');
note.textContent = 'Read this first';
heading.before(note);   // inserts note BEFORE the h2
heading.after(note);    // moves note AFTER the h2

// insertAdjacentElement(position, element)
// Positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
//
// <!-- beforebegin -->
// <div>
//   <!-- afterbegin -->
//   existing content
//   <!-- beforeend -->
// </div>
// <!-- afterend -->

const sidebar = document.querySelector('.sidebar');
sidebar.insertAdjacentElement('afterbegin', note); // first child inside sidebar

// insertAdjacentHTML - for strings of HTML (trusted content only)
sidebar.insertAdjacentHTML('beforeend', '<p class="footer-note">Updated</p>');

// replaceWith() - replace an element with another
const oldBtn = document.querySelector('.old-btn');
const newBtn = document.createElement('button');
newBtn.textContent = 'New Button';
oldBtn.replaceWith(newBtn);
Use DocumentFragment for batch inserts

When inserting many elements in a loop, add them to a DocumentFragment first, then insert the fragment once. This triggers only one reflow instead of one per element - a significant performance gain for large lists.

const frag = document.createDocumentFragment();
items.forEach(item => {
    const li = document.createElement("li");
    li.textContent = item;
    frag.append(li);
});
list.append(frag); // single DOM update

Removing Elements

JavaScript
// remove() - modern, call on the element itself
document.querySelector('.modal').remove();

// removeChild() - older, requires parent reference
const parent = document.querySelector('.list');
const child  = parent.querySelector('.item');
parent.removeChild(child);

// Remove all children - fastest way to clear a container
const container = document.querySelector('.gallery');
container.innerHTML = '';   // simple but destroys event listeners on children
// Alternative that keeps references (useful if children have listeners):
while (container.firstChild) {
    container.removeChild(container.firstChild);
}
// Or modern replaceChildren() with no args:
container.replaceChildren();

// Detach and re-attach (temporarily remove for batch updates)
const table = document.querySelector('table');
const parent2 = table.parentNode;
const nextSib = table.nextSibling;
table.remove();                        // detach from DOM
// ... make many DOM changes to table ...
parent2.insertBefore(table, nextSib); // re-attach

// Remove elements matching a condition
document.querySelectorAll('.error-message').forEach(el => el.remove());

Cloning Nodes

JavaScript
// cloneNode(deep)
// deep=true  - clone element AND all its descendants
// deep=false - clone only the element itself, no children

const original = document.querySelector('.card');
const shallowCopy = original.cloneNode(false); // empty clone, no children
const deepCopy    = original.cloneNode(true);  // full copy with all children

// Clone does NOT copy event listeners - re-attach them
deepCopy.querySelector('button').addEventListener('click', handleClick);

document.querySelector('.card-grid').append(deepCopy);

// Practical: card list from a template
const template = document.querySelector('#card-template'); // <div class="card">...</div>

function createCard(data) {
    const card = template.cloneNode(true);
    card.removeAttribute('id'); // remove id to avoid duplicates
    card.querySelector('.card-title').textContent = data.title;
    card.querySelector('.card-body').textContent  = data.body;
    card.dataset.id = data.id;
    return card;
}

const frag = document.createDocumentFragment();
products.forEach(p => frag.append(createCard(p)));
document.querySelector('.card-grid').append(frag);

// <template> element - best practice for HTML templates
// Content inside <template> is inert (not rendered, not fetched)
const tmpl = document.querySelector('template');
const clone = tmpl.content.cloneNode(true); // clones the template content
document.body.append(clone);
Prefer the <template> element

The HTML <template> element is purpose-built for reusable markup. Its content is a DocumentFragment - not rendered, images not loaded, scripts not executed until you clone and insert it. Use it instead of hidden elements for cloning patterns.

Working with Multiple Elements

querySelectorAll returns a static NodeList. Convert to an array to use all array methods.

JavaScript
// NodeList to Array
const items = Array.from(document.querySelectorAll('.item'));
// or: const items = [...document.querySelectorAll('.item')];

// forEach is available on NodeList directly (no conversion needed)
document.querySelectorAll('.card').forEach(card => {
    card.classList.add('loaded');
});

// But map/filter/reduce require an array
const titles = Array.from(document.querySelectorAll('h2'))
    .map(h2 => h2.textContent);

// Filter elements by a condition
const errorInputs = Array.from(document.querySelectorAll('input'))
    .filter(input => input.value.trim() === '');

// Find the first matching element in a set
const firstActive = Array.from(document.querySelectorAll('.tab'))
    .find(tab => tab.classList.contains('active'));

// Apply changes to a filtered subset
Array.from(document.querySelectorAll('.price'))
    .filter(el => parseFloat(el.dataset.amount) > 100)
    .forEach(el => el.classList.add('premium'));

// Collect values from inputs
const values = Array.from(document.querySelectorAll('input[name="tag"]'))
    .map(input => input.value)
    .filter(Boolean); // remove empty strings

Practical DOM Patterns

JavaScript
// Pattern 1: Toggle visibility
function toggleMenu(menuEl) {
    menuEl.classList.toggle('is-open');
    menuEl.setAttribute('aria-expanded',
        menuEl.classList.contains('is-open').toString()
    );
}

// Pattern 2: Show/hide with animation (add class, wait for transition)
function dismissAlert(alertEl) {
    alertEl.classList.add('fade-out');
    alertEl.addEventListener('transitionend', () => alertEl.remove(), { once: true });
}

// Pattern 3: Render a list from data
function renderList(container, items, renderFn) {
    container.replaceChildren(); // clear existing
    if (items.length === 0) {
        container.append('No items found.');
        return;
    }
    const frag = document.createDocumentFragment();
    items.forEach(item => frag.append(renderFn(item)));
    container.append(frag);
}

// Pattern 4: Highlight active nav link
function setActiveLink(links, activeHref) {
    links.forEach(link => {
        link.classList.toggle('active', link.href === activeHref);
        link.setAttribute('aria-current',
            link.href === activeHref ? 'page' : 'false'
        );
    });
}

// Pattern 5: Lazy-load attribute swap
document.querySelectorAll('img[data-src]').forEach(img => {
    img.src = img.dataset.src;
    img.removeAttribute('data-src');
});
Avoid reading layout properties in a loop

Reading offsetWidth, getBoundingClientRect(), or any property that forces the browser to recalculate layout inside a write loop causes layout thrashing. Batch reads before writes: read all values first, then apply all DOM changes.

Frequently Asked Questions

element.remove() is the modern approach - call it directly on the element you want to remove. parentElement.removeChild(child) is the older approach requiring a reference to the parent. Prefer remove() since it is simpler and supported in all modern browsers. Example: document.querySelector(".modal").remove().

No. cloneNode(true) copies the element and all its child nodes and attributes, but event listeners attached via addEventListener are NOT copied. You must re-attach event listeners to the clone manually. Inline event handlers (like onclick="..." attributes) are copied since they are just attributes, but this pattern is discouraged.

appendChild(node) accepts only a single Node and returns the appended node. append() is more flexible - it accepts multiple arguments, mixes Node objects and strings (strings become text nodes automatically), and returns undefined. Prefer append() for modern code: el.append("Text ", span, "more text").

When you set element.innerHTML = "...", the browser destroys all existing child nodes and creates new ones from the HTML string. Any event listeners attached to the destroyed nodes are lost. This is another reason to prefer createElement + appendChild/append() when you need to keep event listeners, and to use event delegation on stable parent elements.