Creating DOM Elements: Dynamically Building Page Content
Why Create Elements Dynamically
Static HTML can only display fixed content. Modern web pages often need to dynamically update the interface based on user actions, server data, or other conditions—adding new list items, inserting notification messages, rendering data fetched from APIs, etc. These all require creating new DOM elements in JavaScript.
There are several main methods for dynamically creating elements, each with its applicable scenarios. Choosing the right method affects not only code readability but also page performance and security.
Basic Methods for Creating Elements
createElement
document.createElement() is the core method for creating new elements:
// Create a new div element
const div = document.createElement("div");
// Create a button
const button = document.createElement("button");
// Create a link
const link = document.createElement("a");Newly created elements only exist in memory and haven't been added to the page yet. Before adding, you can set various properties:
const card = document.createElement("div");
card.className = "card";
card.id = "user-card";
const title = document.createElement("h2");
title.textContent = "John Doe";
title.classList.add("card-title");
const description = document.createElement("p");
description.textContent = "Software Engineer at TechCorp";createTextNode
document.createTextNode() is used to create plain text nodes:
const text = document.createTextNode("Hello, World!");
// Usually used in combination with elements
const paragraph = document.createElement("p");
paragraph.appendChild(text);However, in most cases, using the element's textContent property directly is more concise:
const paragraph = document.createElement("p");
paragraph.textContent = "Hello, World!";textContent automatically creates text nodes and escapes content to prevent XSS attacks.
createDocumentFragment
When you need to insert multiple elements at once, using DocumentFragment can significantly improve performance:
// Create a document fragment
const fragment = document.createDocumentFragment();
// Add multiple elements to the fragment
for (let i = 1; i <= 100; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
// Insert all elements at once
document.querySelector("ul").appendChild(fragment);DocumentFragment is a "virtual" container that doesn't belong to the actual DOM tree. Operations performed on it don't trigger page reflow; only when the entire fragment is inserted into the DOM does it trigger a single update.
Inserting Elements into the DOM
Created elements need to be inserted into the document to be displayed. There are multiple insertion methods for different position requirements.
appendChild
Add an element as the last child node of the parent element:
const container = document.querySelector(".container");
const newElement = document.createElement("div");
newElement.textContent = "New content";
container.appendChild(newElement);appendChild returns the added node, allowing chaining:
const list = document.querySelector("ul");
const item = document.createElement("li");
const text = document.createTextNode("New item");
list.appendChild(item).appendChild(text);If the added node already exists in the document, it will be moved to the new location:
const firstItem = document.querySelector("#item-1");
const lastContainer = document.querySelector("#last-container");
// firstItem will be removed from its original location and added to lastContainer
lastContainer.appendChild(firstItem);insertBefore
Insert an element before the specified reference node:
const parent = document.querySelector(".list");
const newItem = document.createElement("li");
newItem.textContent = "Inserted item";
const referenceNode = parent.children[2]; // Third child element
parent.insertBefore(newItem, referenceNode);If the reference node is null, the effect is the same as appendChild:
parent.insertBefore(newItem, null); // Add to the endprepend and append
These two modern methods are more flexible and can insert multiple nodes or text simultaneously:
const container = document.querySelector(".container");
// append - Add to the end
container.append(
"Some text", // Strings are automatically converted to text nodes
document.createElement("span"),
document.createElement("div")
);
// prepend - Add to the beginning
container.prepend(document.createElement("header"), "Header text");Differences from appendChild:
append/prependcan accept multiple parameters- Can directly insert strings
- No return value
before and after
Insert before or after the element itself:
const target = document.querySelector(".target");
// Insert before target
target.before(document.createElement("div"), "Before text");
// Insert after target
target.after("After text", document.createElement("span"));replaceWith
Replace an existing node with a new node:
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "I replaced the old element";
oldElement.replaceWith(newElement);Can also replace with multiple nodes or text simultaneously:
oldElement.replaceWith(
"Some text",
document.createElement("span"),
"More text"
);Insertion Position Comparison
const target = document.querySelector(".target");
// Illustration of each method's insertion position
/*
parent.prepend(A) → A becomes first child node
target.before(B) → B inserted before target
[target] → Target element itself
target.after(C) → C inserted after target
parent.append(D) → D becomes last child node
*/Using insertAdjacentHTML and Related Methods
insertAdjacentHTML provides another way to insert content, directly parsing HTML strings:
const container = document.querySelector(".container");
container.insertAdjacentHTML("beforebegin", "<p>Before container</p>");
container.insertAdjacentHTML("afterbegin", "<p>First child</p>");
container.insertAdjacentHTML("beforeend", "<p>Last child</p>");
container.insertAdjacentHTML("afterend", "<p>After container</p>");Meanings of the four position parameters:
<!-- beforebegin -->
<div class="container">
<!-- afterbegin -->
<p>Existing content</p>
<!-- beforeend -->
</div>
<!-- afterend -->Similar methods also exist:
// insertAdjacentText - Insert plain text (escapes HTML)
element.insertAdjacentText("beforeend", '<script>alert("safe")</script>');
// Result: Display text "<script>alert("safe")</script>"
// insertAdjacentElement - Insert element node
const newDiv = document.createElement("div");
element.insertAdjacentElement("afterend", newDiv);Using innerHTML and Its Risks
innerHTML is a convenient property for setting or getting an element's inner HTML:
const container = document.querySelector(".container");
// Set HTML content
container.innerHTML = "<h1>Title</h1><p>Paragraph</p>";
// Append content (will re-parse entire innerHTML)
container.innerHTML += "<footer>Footer</footer>";Problems with innerHTML:
- Performance issues: Each setting completely reparses and rebuilds the DOM
- Event loss: Event listeners on original elements are cleared
- Security risks: If it contains user input, may lead to XSS attacks
// ❌ Dangerous: User input may contain malicious scripts
const userInput = '<img src=x onerror="alert(document.cookie)">';
container.innerHTML = userInput; // XSS vulnerability!
// ✅ Safe: Use textContent to escape HTML
container.textContent = userInput; // Display original text
// ✅ Safe: Manually escape
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
container.innerHTML = `<p>${escapeHtml(userInput)}</p>`;Cloning Elements
The cloneNode method can copy existing elements:
const original = document.querySelector(".template");
// Shallow clone: Only copy the element itself, not child elements
const shallowClone = original.cloneNode(false);
// Deep clone: Copy the element and all its descendants
const deepClone = original.cloneNode(true);
// Cloned elements are not in the document, need to insert
document.body.appendChild(deepClone);Things to note when cloning:
- Cloning copies all attributes (including
id, which may result in duplicate IDs) - Event listeners are not copied
- If the original element has an
id, it's recommended to modify the cloned element'sid
const template = document.querySelector("#card-template");
const clone = template.cloneNode(true);
// Modify ID to avoid duplication
clone.id = "card-" + Date.now();
// Add to page
document.querySelector(".cards").appendChild(clone);Using the template Element
HTML5's <template> element is specifically designed for defining reusable HTML fragments:
<template id="user-card-template">
<article class="user-card">
<img class="avatar" src="" alt="" />
<h3 class="name"></h3>
<p class="bio"></p>
<button class="follow-btn">Follow</button>
</article>
</template>function createUserCard(user) {
const template = document.querySelector("#user-card-template");
// Clone template content
const clone = template.content.cloneNode(true);
// Fill data
clone.querySelector(".avatar").src = user.avatar;
clone.querySelector(".avatar").alt = user.name;
clone.querySelector(".name").textContent = user.name;
clone.querySelector(".bio").textContent = user.bio;
// Add event
clone.querySelector(".follow-btn").addEventListener("click", () => {
followUser(user.id);
});
return clone;
}
// Usage
const container = document.querySelector(".user-list");
const users = [
{ id: 1, name: "John Doe", bio: "Developer", avatar: "/avatars/john.jpg" },
{ id: 2, name: "Jane Smith", bio: "Designer", avatar: "/avatars/jane.jpg" },
];
users.forEach((user) => {
container.appendChild(createUserCard(user));
});Advantages of <template>:
- Template content doesn't render on the page
- Doesn't execute scripts or load resources (like images)
- Can write directly in HTML, easy to maintain
- Supports complex HTML structures
Practical Application Examples
Dynamic List Management
class TodoList {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
this.items = [];
}
addItem(text) {
const id = Date.now();
this.items.push({ id, text, completed: false });
const li = document.createElement("li");
li.dataset.id = id;
li.className = "todo-item";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.addEventListener("change", () => this.toggleItem(id));
const span = document.createElement("span");
span.textContent = text;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "×";
deleteBtn.className = "delete-btn";
deleteBtn.addEventListener("click", () => this.removeItem(id));
li.append(checkbox, span, deleteBtn);
this.container.appendChild(li);
}
removeItem(id) {
const index = this.items.findIndex((item) => item.id === id);
if (index > -1) {
this.items.splice(index, 1);
const li = this.container.querySelector(`[data-id="${id}"]`);
if (li) {
li.remove();
}
}
}
toggleItem(id) {
const item = this.items.find((item) => item.id === id);
if (item) {
item.completed = !item.completed;
const li = this.container.querySelector(`[data-id="${id}"]`);
if (li) {
li.classList.toggle("completed", item.completed);
}
}
}
render() {
// Use DocumentFragment for batch rendering
const fragment = document.createDocumentFragment();
this.items.forEach((item) => {
const li = document.createElement("li");
li.dataset.id = item.id;
li.className = "todo-item" + (item.completed ? " completed" : "");
li.innerHTML = `
<input type="checkbox" ${item.completed ? "checked" : ""}>
<span>${this.escapeHtml(item.text)}</span>
<button class="delete-btn">×</button>
`;
fragment.appendChild(li);
});
this.container.innerHTML = "";
this.container.appendChild(fragment);
this.attachEventListeners();
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
attachEventListeners() {
// Use event delegation
this.container.addEventListener("change", (e) => {
if (e.target.type === "checkbox") {
const li = e.target.closest("li");
if (li) {
this.toggleItem(Number(li.dataset.id));
}
}
});
this.container.addEventListener("click", (e) => {
if (e.target.matches(".delete-btn")) {
const li = e.target.closest("li");
if (li) {
this.removeItem(Number(li.dataset.id));
}
}
});
}
}
// Usage
const todoList = new TodoList("#todo-container");
todoList.addItem("Learn JavaScript");
todoList.addItem("Build a project");Modal Generator
function createModal({ title, content, onConfirm, onCancel }) {
// Create overlay
const overlay = document.createElement("div");
overlay.className = "modal-overlay";
// Create modal
const modal = document.createElement("div");
modal.className = "modal";
// Title bar
const header = document.createElement("div");
header.className = "modal-header";
const titleEl = document.createElement("h2");
titleEl.textContent = title;
const closeBtn = document.createElement("button");
closeBtn.className = "modal-close";
closeBtn.textContent = "×";
closeBtn.setAttribute("aria-label", "Close");
header.append(titleEl, closeBtn);
// Content area
const body = document.createElement("div");
body.className = "modal-body";
if (typeof content === "string") {
body.textContent = content;
} else if (content instanceof Node) {
body.appendChild(content);
}
// Footer buttons
const footer = document.createElement("div");
footer.className = "modal-footer";
const cancelBtn = document.createElement("button");
cancelBtn.className = "btn btn-secondary";
cancelBtn.textContent = "Cancel";
const confirmBtn = document.createElement("button");
confirmBtn.className = "btn btn-primary";
confirmBtn.textContent = "Confirm";
footer.append(cancelBtn, confirmBtn);
// Assemble modal
modal.append(header, body, footer);
overlay.appendChild(modal);
// Close function
function closeModal() {
overlay.remove();
}
// Bind events
closeBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeModal();
});
cancelBtn.addEventListener("click", () => {
onCancel?.();
closeModal();
});
confirmBtn.addEventListener("click", () => {
onConfirm?.();
closeModal();
});
// Add to page
document.body.appendChild(overlay);
// Focus on confirm button
confirmBtn.focus();
return { close: closeModal };
}
// Usage example
createModal({
title: "Confirm Action",
content: "Are you sure you want to delete this item?",
onConfirm: () => console.log("Confirmed!"),
onCancel: () => console.log("Cancelled"),
});Infinite Scroll Loading
class InfiniteScroll {
constructor(containerSelector, loadMore) {
this.container = document.querySelector(containerSelector);
this.loadMore = loadMore;
this.page = 1;
this.loading = false;
this.hasMore = true;
this.init();
}
init() {
// Initial load
this.load();
// Listen to scroll
window.addEventListener("scroll", () => {
if (this.shouldLoadMore()) {
this.load();
}
});
}
shouldLoadMore() {
if (this.loading || !this.hasMore) return false;
const containerBottom = this.container.getBoundingClientRect().bottom;
const threshold = window.innerHeight * 1.5;
return containerBottom <= threshold;
}
async load() {
this.loading = true;
this.showLoader();
try {
const items = await this.loadMore(this.page);
if (items.length === 0) {
this.hasMore = false;
this.showEndMessage();
} else {
this.renderItems(items);
this.page++;
}
} catch (error) {
this.showError(error.message);
} finally {
this.loading = false;
this.hideLoader();
}
}
renderItems(items) {
const fragment = document.createDocumentFragment();
items.forEach((item) => {
const article = document.createElement("article");
article.className = "card";
article.innerHTML = `
<h3>${this.escapeHtml(item.title)}</h3>
<p>${this.escapeHtml(item.description)}</p>
`;
fragment.appendChild(article);
});
this.container.appendChild(fragment);
}
showLoader() {
let loader = this.container.querySelector(".loader");
if (!loader) {
loader = document.createElement("div");
loader.className = "loader";
loader.textContent = "Loading...";
this.container.appendChild(loader);
}
loader.style.display = "block";
}
hideLoader() {
const loader = this.container.querySelector(".loader");
if (loader) {
loader.style.display = "none";
}
}
showEndMessage() {
const message = document.createElement("p");
message.className = "end-message";
message.textContent = "No more items to load";
this.container.appendChild(message);
}
showError(message) {
const error = document.createElement("div");
error.className = "error-message";
error.textContent = `Error: ${message}`;
this.container.appendChild(error);
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
}
// Usage example
const infiniteScroll = new InfiniteScroll("#content", async (page) => {
const response = await fetch(`/api/items?page=${page}`);
return response.json();
});Performance Optimization Recommendations
Batch Operations
// ❌ Bad: Frequent reflow triggers
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
list.appendChild(li);
}
// ✅ Good: Use DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
list.appendChild(fragment);Offline Operations
// ✅ Remove from DOM first, modify, then add back
const container = document.querySelector(".container");
const parent = container.parentNode;
parent.removeChild(container);
// Perform extensive modifications
for (let i = 0; i < 1000; i++) {
container.appendChild(document.createElement("div"));
}
parent.appendChild(container);Virtual List
For very long lists, consider only rendering elements within the visible area:
// Simplified example
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.container.style.height = items.length * itemHeight + "px";
this.container.style.position = "relative";
window.addEventListener("scroll", () => this.render());
this.render();
}
render() {
const scrollTop = window.scrollY;
const containerTop = this.container.offsetTop;
const viewportHeight = window.innerHeight;
const startIndex = Math.floor((scrollTop - containerTop) / this.itemHeight);
const endIndex =
startIndex + Math.ceil(viewportHeight / this.itemHeight) + 1;
this.container.innerHTML = "";
for (
let i = Math.max(0, startIndex);
i < Math.min(this.items.length, endIndex);
i++
) {
const div = document.createElement("div");
div.style.position = "absolute";
div.style.top = i * this.itemHeight + "px";
div.style.height = this.itemHeight + "px";
div.textContent = this.items[i];
this.container.appendChild(div);
}
}
}Summary
Dynamically creating DOM elements is a core skill in modern web development. Choosing the right methods can make your code cleaner and more performant:
| Scenario | Recommended Method |
|---|---|
| Create single element | createElement + appendChild |
| Create multiple elements | DocumentFragment |
| Add to end | append / appendChild |
| Add to beginning | prepend |
| Insert at position | before / after / insertBefore |
| Create from HTML string | insertAdjacentHTML (watch security) |
| Create from template | <template> + cloneNode |
Best practices:
- Try to batch operations to minimize DOM updates
- Use
textContentrather thaninnerHTMLfor user input - Leverage event delegation to reduce event listeners
- Consider virtual lists for large amounts of data