Real applications do not just modify what is already on the page, they build entire interfaces on the fly. Think about a social media feed where new posts appear as you scroll, or a todo list where items appear as you type. This is dynamic element creation in action.
The lifecycle of a DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify. element
Every created element goes through three stages:
- Creation: you create the element in JavaScript memory
- Configuration: you set its content, classes, attributes, and event listeners
- Insertion: you attach it to the DOM tree, making it visible
Until step 3, the element exists only in memory. The user cannot see it.
Creating elements
The foundation is document.createElement():
const paragraph = document.createElement('p');
paragraph.textContent = 'This is a dynamically created paragraph!';
paragraph.classList.add('intro-text');
paragraph.id = 'dynamic-para';
// Exists in memory but NOT on the page yetBuilding complex structures
const card = document.createElement('div');
card.classList.add('card');
const title = document.createElement('h3');
title.textContent = 'Product Name';
const description = document.createElement('p');
description.textContent = 'An amazing product.';
const button = document.createElement('button');
button.textContent = 'Buy Now';
button.classList.add('btn', 'btn-primary');
card.appendChild(title);
card.appendChild(description);
card.appendChild(button);This is more verbose than innerHTML, but it is safe by default. Every piece of text goes through textContent, which cannot execute scripts. Compare:
// SAFE: textContent escapes everything automatically
title.textContent = userInput; // "<script>alert('hack')</script>" displays as plain text
// DANGEROUS: innerHTML executes scripts from user input
title.innerHTML = userInput; // "<script>alert('hack')</script>" runs the script!container.innerHTML = '<div class="card"><h3>' + title + '</h3></div>' instead of using createElement. This is faster to write but creates an XSS vulnerability if any variable contains user input. The variable could contain <img src=x onerror="steal(document.cookie)"> and innerHTML would execute it. Even when AI does use createElement, it often sets content with el.innerHTML = userInput rather than el.textContent = userInput. Always use createElement combined with textContent when building elements from dynamic data. Reserve innerHTML for static HTML templates that you wrote yourself and control entirely.Inserting elements
Once your element is ready, attach it to the document:
| Method | Position | Notes |
|---|---|---|
parent.appendChild(el) | Last child | Classic, returns the appended element |
parent.append(el) | Last child | Modern, accepts multiple nodes and strings |
parent.prepend(el) | First child | Adds at the beginning |
parent.insertBefore(el, ref) | Before reference | Classic, requires parent context |
ref.before(el) | Before sibling | Modern, called on the reference element |
ref.after(el) | After sibling | Modern, called on the reference element |
const list = document.querySelector('#todo-list');
const newItem = document.createElement('li');
newItem.textContent = 'Buy groceries';
list.appendChild(newItem); // adds as last child
// Or insert before a specific element
const firstItem = list.querySelector('li');
list.insertBefore(newItem, firstItem); // adds before first iteminsertAdjacentHTML
For quick HTML insertion (use only with trusted strings):
container.insertAdjacentHTML('beforeend', '<p>Last child inside</p>');
container.insertAdjacentHTML('afterbegin', '<p>First child inside</p>');innerHTML, insertAdjacentHTML parses HTML strings. Never use it with user-generated content.Removing elements
const item = document.querySelector('#item-123');
item.remove(); // gone from the page
// Classic way (via parent)
const parent = document.querySelector('#list');
const child = document.querySelector('#item-123');
parent.removeChild(child); // returns the removed elementCleaning up before removal
Before removing complex components, clean up associated resources:
function removeTodoItem(itemElement) {
const deleteBtn = itemElement.querySelector('.delete-btn');
deleteBtn.removeEventListener('click', handleDelete);
clearTimeout(itemElement.dataset.timeoutId);
itemElement.remove();
}Performance: batchingWhat is batching?Grouping multiple state updates together into a single re-render cycle instead of re-rendering after each individual update. with DocumentFragment
Creating many elements individually is slow because each insertion triggers browser reflow. Use DocumentFragment to batch:
// Slow: 1000 DOM updates
const list = document.querySelector('#big-list');
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
list.appendChild(item); // triggers reflow each time
}
// Fast: 1 DOM update
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('li');
item.textContent = `Item ${i}`;
fragment.appendChild(item); // no reflow - fragment is in memory
}
list.appendChild(fragment); // single reflowReal-world pattern: dynamic todo list
function createTodoElement(text, id) {
const li = document.createElement('li');
li.classList.add('todo-item');
li.dataset.id = id;
const span = document.createElement('span');
span.textContent = text; // safe - uses textContent, not innerHTML
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.classList.add('btn-delete');
li.appendChild(span);
li.appendChild(deleteBtn);
return li;
}
// Usage with event delegation on the parent (no per-item listeners needed)
const todoList = document.querySelector('#todos');
todoList.appendChild(createTodoElement('Buy groceries', 123));
todoList.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-delete')) {
e.target.closest('.todo-item').remove();
}
});Notice the pattern: createElement + textContent for safe content, event delegationWhat is event delegation?Attaching one event listener to a parent element instead of many listeners to each child, using event bubbling to catch events from children. on the parent for efficient click handling, and closest() to find the parent li from the button click.
Replacing and cloning
// Replace an element
const oldEl = document.querySelector('.old');
const newEl = document.createElement('div');
newEl.textContent = 'Replacement!';
oldEl.replaceWith(newEl);
// Clone an element (true = deep clone with children)
const template = document.querySelector('.card-template');
const clone = template.cloneNode(true);
clone.querySelector('.title').textContent = 'New Title';
document.querySelector('.container').appendChild(clone);Quick reference
| Task | Code |
|---|---|
| Create element | document.createElement('div') |
| Set text (safe) | el.textContent = 'text' |
| Add class | el.classList.add('name') |
| Append as last child | parent.appendChild(el) |
| Prepend as first child | parent.prepend(el) |
| Insert before sibling | ref.before(el) |
| Remove element | el.remove() |
| Batch inserts | document.createDocumentFragment() |
| Clone element | el.cloneNode(true) |
| Replace element | el.replaceWith(newEl) |