Manipulating the DOM

โ–ถ Try It Yourself

Reading the DOM is only half the story โ€” the real power is in changing it. Every interactive UI feature involves creating new elements, updating content and attributes, toggling styles, or removing elements from the page. In this lesson you will master all DOM manipulation techniques: creating and inserting elements, modifying content safely, working with classes and attributes, and cloning nodes. You will also learn why innerHTML can be dangerous and when to use textContent instead.

Creating and Inserting Elements

Method Effect Notes
document.createElement('tag') Create new element in memory Not in the DOM yet โ€” must insert
document.createTextNode('text') Create text node Safe โ€” no HTML parsing
document.createDocumentFragment() Lightweight container for batch inserts Avoids multiple reflows
parent.appendChild(child) Add child at end of parent Moves element if already in DOM
parent.insertBefore(new, ref) Insert before reference node Classic API โ€” prefer insertAdjacentElement
el.insertAdjacentElement(pos, new) Insert relative to element pos: beforebegin / afterbegin / beforeend / afterend
el.insertAdjacentHTML(pos, html) Parse and insert HTML string Fast โ€” but dangerous with user input
el.append(...nodes) Append one or more nodes/strings at end Modern โ€” accepts strings directly
el.prepend(...nodes) Insert at beginning Modern โ€” accepts strings directly
el.replaceWith(...nodes) Replace element with new node(s) Removes self and inserts replacement
el.remove() Remove element from DOM Modern โ€” no need for parentNode.removeChild
el.cloneNode(deep) Clone element; deep=true includes children Does not clone event listeners

Content and Attribute Properties

Property / Method Read / Write Safe from XSS? Use For
element.textContent R/W Yes โ€” treats as plain text Text content โ€” always prefer for user data
element.innerHTML R/W No โ€” parses HTML Trusted HTML templates only
element.innerText R/W Yes Visible text only โ€” respects CSS display:none
element.getAttribute(name) R Yes Get any attribute value
element.setAttribute(name, val) W Yes Set any attribute
element.removeAttribute(name) W Yes Remove an attribute
element.hasAttribute(name) R Yes Boolean โ€” attribute exists?
element.dataset.key R/W Yes Read/write data-* attributes
element.id R/W Yes id attribute shortcut
element.value R/W Yes Input current value
element.hidden R/W boolean Yes Toggle hidden attribute

Class Manipulation

Method Effect
el.classList.add('a', 'b') Add one or more classes
el.classList.remove('a', 'b') Remove one or more classes
el.classList.toggle('cls') Add if absent, remove if present
el.classList.toggle('cls', bool) Add if bool is true, remove if false
el.classList.contains('cls') Boolean โ€” has class?
el.classList.replace('old', 'new') Replace one class with another
Note: textContent and innerHTML have a critical security difference. textContent always treats the value as plain text โ€” any HTML characters are escaped automatically. innerHTML parses the string as HTML โ€” setting it with user-supplied data can execute malicious scripts (XSS). Always use textContent for user data. Use innerHTML only for trusted, developer-controlled HTML templates.
Tip: Use DocumentFragment when inserting many elements at once. Appending to a fragment does not trigger reflow. When you append the completed fragment to the DOM, the browser only reflows once. For inserting 100 list items, this can be 100x faster than inserting each item individually into the live DOM.
Warning: cloneNode(true) clones an element and all its children โ€” but it does NOT clone event listeners added with addEventListener. If the original has click handlers attached programmatically, the clone will be silent. You must re-attach listeners to the clone manually, or use event delegation on a parent instead.

Basic Example

// โ”€โ”€ Create and insert โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const list = document.querySelector('#todo-list');

// Create a single item
const li = document.createElement('li');
li.className   = 'todo-item';
li.textContent = 'Learn the DOM';   // safe โ€” not innerHTML
list.appendChild(li);

// insertAdjacentHTML positions:
//   beforebegin โ€” before the element itself
//   afterbegin  โ€” first child of element
//   beforeend   โ€” last child of element (same as append)
//   afterend    โ€” after the element itself
list.insertAdjacentHTML('beforeend', `
    <li class="todo-item">Build a project</li>
`);

// โ”€โ”€ DocumentFragment for batch inserts โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const todos = ['Write tests', 'Deploy app', 'Celebrate'];
const frag  = document.createDocumentFragment();

todos.forEach(text => {
    const item = document.createElement('li');
    item.className   = 'todo-item';
    item.textContent = text;   // safe for user-supplied text
    frag.appendChild(item);
});

list.appendChild(frag);   // single reflow โ€” all items added at once

// โ”€โ”€ textContent vs innerHTML โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const output = document.querySelector('#output');

const userInput = '<img src=x onerror="alert(1)">';

output.innerHTML   = userInput;   // โŒ DANGEROUS โ€” executes script!
output.textContent = userInput;   // โœ… SAFE โ€” displays as text

// โ”€โ”€ Class manipulation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const card = document.querySelector('.card');

card.classList.add('featured', 'highlighted');
card.classList.remove('draft');
card.classList.toggle('expanded');               // add if missing, remove if present
card.classList.toggle('visible', true);          // force-add
card.classList.toggle('disabled', false);        // force-remove
card.classList.replace('old-theme', 'new-theme');

console.log(card.classList.contains('featured')); // true

// โ”€โ”€ Attributes and dataset โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const img = document.querySelector('img');

img.setAttribute('alt', 'A beautiful sunrise');
img.setAttribute('loading', 'lazy');
img.removeAttribute('title');
console.log(img.getAttribute('src'));      // current src value
console.log(img.hasAttribute('loading')); // true

// data-* attributes via dataset
const btn = document.querySelector('[data-action]');
console.log(btn.dataset.action);      // 'delete'
console.log(btn.dataset.itemId);      // '42' (data-item-id โ†’ camelCase)
btn.dataset.confirmed = 'true';       // sets data-confirmed="true"

// โ”€โ”€ Remove elements โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
document.querySelector('.banner')?.remove();   // remove if exists

// Remove all children of a container
const container = document.querySelector('#notifications');
container.replaceChildren();   // fastest way to empty a container

// โ”€โ”€ Clone a template card โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const template = document.querySelector('.card-template');
const clone    = template.cloneNode(true);
clone.querySelector('.card-title').textContent = 'New Card Title';
clone.querySelector('.card-body').textContent  = 'New card body text.';
clone.classList.remove('card-template');
document.querySelector('#card-grid').appendChild(clone);

How It Works

Step 1 โ€” createElement Creates Detached Nodes

document.createElement('li') creates a new element object in memory but does not attach it to the DOM tree yet. You configure it (set class, text, attributes) while it is detached โ€” no screen updates happen. Only when you call appendChild, append, or an insert method does the element appear on screen.

Step 2 โ€” Every DOM Change Can Trigger a Reflow

When you insert, remove, or resize an element, the browser may need to recalculate the positions of other elements (reflow) and repaint the screen. Triggering reflows in a loop โ€” inserting 100 items one by one โ€” is expensive. DocumentFragment batches all inserts into one reflow by keeping nodes off-DOM until the fragment is appended.

Step 3 โ€” textContent Is Always Safe

el.textContent = str replaces all child nodes with a single text node containing str, with all HTML characters escaped automatically. <script> becomes the literal text <script> on screen. Use this for any user-supplied or external data.

Step 4 โ€” classList Methods Are Idempotent

classList.add('active') does nothing if the class is already present. classList.remove('active') does nothing if it is absent. This means you do not need to check before calling โ€” just call and the DOM will be in the correct state. The toggle overload toggle('cls', bool) is a clean way to conditionally apply a class based on any boolean expression.

Step 5 โ€” dataset Converts Attribute Names Automatically

HTML attribute data-item-id becomes element.dataset.itemId โ€” kebab-case is converted to camelCase. Reading dataset.itemId returns a string (always โ€” even numbers). Writing dataset.itemId = 42 sets data-item-id="42" in the HTML. Delete with delete element.dataset.itemId.

Real-World Example: Dynamic Todo List

// todo-list.js

class TodoList {
    #items = [];
    #container;
    #input;
    #counter;

    constructor(selector) {
        this.#container = document.querySelector(selector);
        this.#input     = this.#container.querySelector('.todo-input');
        this.#counter   = this.#container.querySelector('.todo-count');
        this.#container.querySelector('.todo-form')
            .addEventListener('submit', e => {
                e.preventDefault();
                const text = this.#input.value.trim();
                if (text) { this.add(text); this.#input.value = ''; }
            });
        this.#container.querySelector('.todo-items')
            .addEventListener('click', e => {
                const li = e.target.closest('[data-id]');
                if (!li) return;
                if (e.target.matches('.delete-btn'))  this.remove(li.dataset.id);
                if (e.target.matches('.toggle-btn'))  this.toggle(li.dataset.id);
            });
    }

    add(text) {
        const id   = Date.now().toString();
        this.#items.push({ id, text, done: false });
        this.#render();
    }

    remove(id) {
        this.#items = this.#items.filter(i => i.id !== id);
        this.#render();
    }

    toggle(id) {
        const item = this.#items.find(i => i.id === id);
        if (item) item.done = !item.done;
        this.#render();
    }

    #render() {
        const frag = document.createDocumentFragment();

        this.#items.forEach(({ id, text, done }) => {
            const li = document.createElement('li');
            li.dataset.id = id;
            li.classList.toggle('done', done);

            const span = document.createElement('span');
            span.textContent = text;   // safe โ€” no innerHTML for user text

            const toggleBtn = document.createElement('button');
            toggleBtn.className   = 'toggle-btn';
            toggleBtn.textContent = done ? 'Undo' : 'Done';

            const deleteBtn = document.createElement('button');
            deleteBtn.className   = 'delete-btn';
            deleteBtn.textContent = 'Delete';

            li.append(span, toggleBtn, deleteBtn);
            frag.appendChild(li);
        });

        const list = this.#container.querySelector('.todo-items');
        list.replaceChildren(frag);   // atomic replace โ€” single reflow

        const pending = this.#items.filter(i => !i.done).length;
        this.#counter.textContent = `${pending} item${pending !== 1 ? 's' : ''} remaining`;
    }
}

const todos = new TodoList('#todo-app');

Common Mistakes

Mistake 1 โ€” Using innerHTML with user-supplied data (XSS)

โŒ Wrong โ€” executes any embedded script:

el.innerHTML = userInput;   // XSS vulnerability if input is untrusted

โœ… Correct โ€” textContent for data, innerHTML only for trusted templates:

el.textContent = userInput;   // always safe

Mistake 2 โ€” Forgetting that cloneNode does not clone event listeners

โŒ Wrong โ€” clone has no click handler:

const clone = card.cloneNode(true);
// card had a click listener โ€” clone does NOT
container.appendChild(clone);

โœ… Correct โ€” use event delegation instead of per-element listeners:

container.addEventListener('click', e => {
    const card = e.target.closest('.card');
    if (card) handleCardClick(card);
});

Mistake 3 โ€” Inserting items in a loop without fragment

โŒ Slow โ€” triggers a reflow for each item:

items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item;
    list.appendChild(li);   // reflow on every iteration
});

โœ… Fast โ€” single reflow with fragment:

const frag = document.createDocumentFragment();
items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item;
    frag.appendChild(li);
});
list.appendChild(frag);   // one reflow

▶ Try It Yourself

Quick Reference

Task Code
Create element document.createElement('div')
Set safe text el.textContent = str
Insert at end parent.append(child)
Insert at start parent.prepend(child)
Insert adjacent el.insertAdjacentElement('beforeend', newEl)
Remove element el.remove()
Empty container el.replaceChildren()
Toggle class el.classList.toggle('active', bool)
Read data attribute el.dataset.myKey
Batch insert DocumentFragment โ†’ append to DOM once

🧠 Test Yourself

A user submits a comment. Which property should you use to display it safely in the DOM?





โ–ถ Try It Yourself