Skip to content

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:

javascript
// 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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
const container = document.querySelector(".container");
const newElement = document.createElement("div");
newElement.textContent = "New content";

container.appendChild(newElement);

appendChild returns the added node, allowing chaining:

javascript
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:

javascript
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:

javascript
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:

javascript
parent.insertBefore(newItem, null); // Add to the end

prepend and append

These two modern methods are more flexible and can insert multiple nodes or text simultaneously:

javascript
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/prepend can accept multiple parameters
  • Can directly insert strings
  • No return value

before and after

Insert before or after the element itself:

javascript
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:

javascript
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:

javascript
oldElement.replaceWith(
  "Some text",
  document.createElement("span"),
  "More text"
);

Insertion Position Comparison

javascript
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
*/

insertAdjacentHTML provides another way to insert content, directly parsing HTML strings:

javascript
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:

html
<!-- beforebegin -->
<div class="container">
  <!-- afterbegin -->
  <p>Existing content</p>
  <!-- beforeend -->
</div>
<!-- afterend -->

Similar methods also exist:

javascript
// 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:

javascript
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:

  1. Performance issues: Each setting completely reparses and rebuilds the DOM
  2. Event loss: Event listeners on original elements are cleared
  3. Security risks: If it contains user input, may lead to XSS attacks
javascript
// ❌ 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:

javascript
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's id
javascript
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:

html
<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>
javascript
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

javascript
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");
javascript
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

javascript
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

javascript
// ❌ 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

javascript
// ✅ 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:

javascript
// 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:

ScenarioRecommended Method
Create single elementcreateElement + appendChild
Create multiple elementsDocumentFragment
Add to endappend / appendChild
Add to beginningprepend
Insert at positionbefore / after / insertBefore
Create from HTML stringinsertAdjacentHTML (watch security)
Create from template<template> + cloneNode

Best practices:

  1. Try to batch operations to minimize DOM updates
  2. Use textContent rather than innerHTML for user input
  3. Leverage event delegation to reduce event listeners
  4. Consider virtual lists for large amounts of data