Skip to main content

Modifying the DOM: Creating, Updating, and Deleting Elements

Published: December 18, 2025 Updated: August 30, 2026 Larry Qu 6 min read

The DOM (Document Object Model) is the browser’s in-memory representation of an HTML page as a tree of objects. JavaScript can modify this tree to add, update, and remove elements — this is what makes web pages interactive. Understanding the DOM manipulation APIs well means knowing not just the methods, but when each approach is appropriate and how to avoid common performance pitfalls.

Creating Elements

The cleanest way to create elements programmatically is document.createElement, which gives you a proper DOM element you can configure before inserting:

// Create an element, configure it, then insert it
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = '42';      // sets data-user-id="42"

const name = document.createElement('h3');
name.textContent = 'Alice';      // textContent is safe — no HTML parsing

const email = document.createElement('p');
email.textContent = '[email protected]';

const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Edit';
button.setAttribute('aria-label', 'Edit Alice');

card.append(name, email, button);   // append multiple children in one call
document.getElementById('userList').append(card);

textContent is always safe to use — it sets literal text. innerHTML parses the string as HTML, which is efficient for templates but dangerous with user-supplied data (it creates XSS vulnerabilities).

innerHTML: When to Use It

innerHTML is convenient for inserting template-driven content at once, and it’s faster than building elements with createElement when inserting many nodes. Use it for static templates with no user-supplied data:

// ✅ Safe — static template with no user data interpolated
function renderEmptyState(message) {
  return `
    <div class="empty-state">
      <svg>...</svg>
      <h3>Nothing here yet</h3>
      <p>${message}</p>
    </div>
  `;
}
container.innerHTML = renderEmptyState('Add your first item to get started.');

// ❌ Dangerous — user data in innerHTML creates XSS risk
const userComment = getCommentFromDB();  // could contain <script>alert(1)</script>
container.innerHTML = `<p>${userComment}</p>`;  // NEVER do this

// ✅ Safe way to mix template with user data — set textContent after inserting
const p = document.createElement('p');
container.innerHTML = '<p></p>';
container.querySelector('p').textContent = userComment;  // safe

Inserting Elements

Modern DOM APIs give you several insertion points:

const parent   = document.getElementById('list');
const newItem  = document.createElement('li');
newItem.textContent = 'New item';
const firstItem = parent.firstElementChild;

parent.append(newItem);              // insert as last child
parent.prepend(newItem);             // insert as first child
parent.insertBefore(newItem, firstItem); // insert before a reference node
firstItem.before(newItem);           // insert before firstItem (same result)
firstItem.after(newItem);            // insert after firstItem

// insertAdjacentHTML — efficient for inserting HTML relative to an element
firstItem.insertAdjacentHTML('beforebegin', '<li>Before first</li>');  // before element
firstItem.insertAdjacentHTML('afterbegin',  '<li>First child</li>');   // first child
firstItem.insertAdjacentHTML('beforeend',   '<li>Last child</li>');    // last child
firstItem.insertAdjacentHTML('afterend',    '<li>After first</li>');   // after element

append and prepend accept both strings and DOM nodes; appendChild only accepts nodes. For modern code, append/prepend/before/after cover most needs clearly.

Updating Elements

Updating content and attributes follows a consistent pattern — read with a getter, write with a setter or method:

const element = document.querySelector('.status-badge');

// Text content
element.textContent = 'Active';           // replace text
console.log(element.textContent);         // read text

// Attributes
element.setAttribute('aria-label', 'Status: Active');
element.getAttribute('aria-label');       // 'Status: Active'
element.removeAttribute('disabled');
element.hasAttribute('disabled');         // false

// CSS classes — classList is the preferred API
element.classList.add('active', 'visible');
element.classList.remove('inactive');
element.classList.toggle('highlighted');  // add if absent, remove if present
element.classList.replace('loading', 'loaded');
element.classList.contains('active');     // true

// Inline styles (prefer CSS classes for styling; use style for dynamic values)
element.style.transform = `translateX(${offsetPx}px)`;
element.style.opacity   = '0.5';

// Dataset (data-* attributes)
element.dataset.status = 'active';       // sets data-status="active"
console.log(element.dataset.status);     // 'active'

Prefer CSS classes over inline styles whenever possible — it keeps presentation in CSS where it belongs. Use style only for truly dynamic values like positions calculated in JavaScript.

Removing Elements

// Remove an element directly (modern API)
document.getElementById('toast').remove();

// Remove a child from its parent (older API, still useful when you have the parent)
const list   = document.getElementById('list');
const first  = list.firstElementChild;
list.removeChild(first);

// Clear all children — innerHTML is fast but loses event listeners
list.innerHTML = '';

// Safer alternative — replaceChildren() removes all children, accepts new ones
list.replaceChildren();          // clear
list.replaceChildren(newItem1, newItem2);  // replace with new children

If the elements being removed have event listeners, remove those listeners first (or use event delegation so they don’t need individual listeners).

Cloning Elements

cloneNode(true) deep-copies an element with all its children. cloneNode(false) copies just the element, not its children:

const template = document.getElementById('cardTemplate');

function createCard(user) {
  const card = template.cloneNode(true);  // deep clone
  card.removeAttribute('id');              // avoid duplicate IDs
  card.querySelector('.card-name').textContent  = user.name;
  card.querySelector('.card-email').textContent = user.email;
  card.querySelector('.card-btn').addEventListener('click', () => openUser(user.id));
  return card;
}

Cloning is more efficient than rebuilding the element from scratch when the template structure is complex.

Performance: DocumentFragment for Batch Inserts

Every time you insert an element directly into the live DOM, the browser may trigger a reflow — recalculating layout for the affected area. Inserting 1000 items one by one triggers up to 1000 reflows.

DocumentFragment is an off-screen container. You build the entire structure in the fragment, then insert it once — causing a single reflow:

// ❌ 1000 reflows — one per appendChild
const list = document.getElementById('results');
for (const item of largeDataset) {
  const li = document.createElement('li');
  li.textContent = item.name;
  list.appendChild(li);   // touches the live DOM each time
}

// ✅ 1 reflow — fragment is not in the live DOM until the final insert
const list = document.getElementById('results');
const fragment = document.createDocumentFragment();

for (const item of largeDataset) {
  const li = document.createElement('li');
  li.textContent = item.name;
  fragment.appendChild(li);  // no reflow
}

list.appendChild(fragment);  // single DOM update, single reflow

For very large lists (hundreds to thousands of items), consider virtual rendering instead — only create DOM elements for the items currently visible in the viewport.

Building Dynamic UI Components

Putting it together — a rendering function that builds a component from data without touching the live DOM until it’s ready:

function renderUserList(users) {
  const fragment = document.createDocumentFragment();

  users.forEach(user => {
    const li = document.createElement('li');
    li.className = `user-item ${user.active ? 'active' : 'inactive'}`;
    li.dataset.id = user.id;

    const avatar = document.createElement('img');
    avatar.src = user.avatarUrl;
    avatar.alt = `${user.name}'s avatar`;
    avatar.className = 'user-avatar';

    const info = document.createElement('div');
    info.className = 'user-info';
    info.innerHTML = `
      <strong class="user-name"></strong>
      <span class="user-email"></span>
    `;
    // Set user-supplied text safely via textContent
    info.querySelector('.user-name').textContent  = user.name;
    info.querySelector('.user-email').textContent = user.email;

    li.append(avatar, info);
    fragment.append(li);
  });

  const list = document.getElementById('userList');
  list.replaceChildren(fragment);  // clear and replace in one step
}

Note the pattern: innerHTML sets the structural template (safe, static HTML), then textContent sets the user-supplied data (safe, no parsing). This combines the efficiency of innerHTML with the XSS safety of textContent.

Summary

The key principles for DOM manipulation:

  • Use createElement when building elements that need event listeners or user data
  • Use innerHTML for static templates; never interpolate user-supplied strings into it
  • Batch DOM insertions with DocumentFragment — one insert, one reflow
  • classList is the right API for CSS class changes; avoid element.className = '...'
  • Remove event listeners before removing elements to avoid memory leaks
  • Cache DOM queries — getElementById inside a loop is expensive

Resources

Comments

👍 Was this article helpful?