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 |
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.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.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
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 |