Adding and Removing DOM Nodes

1

Adding and Removing DOM Nodes

▶

One of the most powerful capabilities JavaScript gives you in the browser is the ability to dynamically modify the structure of a web page while it is running. Rather than delivering a static HTML document and leaving it unchanged, you can create brand-new elements on the fly, insert them anywhere in the page, move them around, replace them, clone them, and remove them — all without ever reloading the document. This is the foundation of interactive UIs, single-page applications, and dynamic content rendering. Understanding how to add and remove DOM nodes precisely and efficiently is therefore one of the most essential skills in client-side JavaScript development.

The DOM (Document Object Model) represents your HTML document as a tree of nodes. Every HTML tag is an element node, every piece of visible text is a text node, and these nodes are connected through parent–child and sibling relationships. When you manipulate the DOM programmatically, you are modifying this live tree, and the browser immediately reflects your changes in the rendered page. The techniques covered below walk through every major step of that process: creation, configuration, insertion, cloning, replacement, and removal.

Creating New Elements with createElement()

The entry point for building new DOM content is document.createElement(). You call it on the global document object and pass a valid HTML tag name as a string. The method returns a fully formed element object of the corresponding type, ready for configuration.

// Create a paragraph element
const para = document.createElement('p');

// Create a list item
const item = document.createElement('li');

// Create a section
const section = document.createElement('section');

// Create an image element
const img = document.createElement('img');

The string you pass must be a recognised HTML tag name. If you pass an unrecognised string, the browser creates an HTMLUnknownElement, which is valid JavaScript but will not render with any built-in browser semantics. Capitalisation does not matter for HTML documents — 'P' and 'p' both produce a paragraph — but lowercase is the universal convention.

A critically important point is that creating an element does not attach it to the document. The returned node exists only in memory, completely disconnected from the visible page. Until you explicitly insert it into the DOM tree using one of the insertion methods described later, it has no effect on what the user sees. This separation between creation and insertion is intentional and useful: it lets you fully configure an element before it ever touches the live DOM.

Setting Content and Attributes on New Nodes

Once you have a new element in memory, the next step is to give it content and any necessary attributes. Doing this before insertion is a performance best practice, because every change you make to an element that is already in the live DOM can trigger the browser to recalculate layout (a reflow) or repaint pixels (a repaint). Building the element fully in memory and inserting it only once minimises that overhead.

To add text content, set the textContent property. This is the safest approach because it treats the value as a plain string — any characters that look like HTML tags are escaped and displayed literally, preventing accidental injection of markup.

const para = document.createElement('p');
para.textContent = 'Hello, world! This is a new paragraph.';

If you intentionally want to embed HTML markup inside the new element, use innerHTML instead. Be careful: if any part of the string comes from user input or an untrusted source, setting innerHTML can open your page to cross-site scripting (XSS) attacks.

const container = document.createElement('div');
container.innerHTML = '<strong>Bold text</strong> followed by normal text.';

To set attributes, you have two equivalent options. The setAttribute() method works for any attribute, including custom data attributes and ARIA attributes, and accepts the attribute name and value as two string arguments.

const link = document.createElement('a');
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
link.textContent = 'Visit Example';

Direct property assignment is often more convenient for standard HTML attributes that map cleanly onto JavaScript element properties:

const img = document.createElement('img');
img.src = 'photo.jpg';       // same as setAttribute('src', 'photo.jpg')
img.alt = 'A scenic photo';
img.id = 'hero-image';
img.className = 'responsive-img featured';

Note that class is a reserved word in JavaScript, so the DOM property is named className. Similarly, for (used on <label> elements) maps to the property htmlFor. For all other attributes the property name matches the HTML attribute name.

You can also manipulate the classList API for more surgical class management:

const card = document.createElement('div');
card.classList.add('card', 'card--featured');
card.classList.remove('card--featured');
card.classList.toggle('is-visible');

Inserting Nodes with appendChild() and append()

With your element configured, the next step is to attach it to a parent node in the live DOM. The two most common methods for this are appendChild() and append().

parentElement.appendChild(newNode) attaches newNode as the last child of parentElement. It accepts exactly one node argument and returns the appended node.

const list = document.getElementById('my-list');

const item = document.createElement('li');
item.textContent = 'New list item';

list.appendChild(item); // item is now the last child of list

An important and often surprising behaviour: if you pass appendChild() a node that is already in the DOM, it does not duplicate it — it moves it to the new position. The node is automatically removed from its current location before being re-inserted.

The append() method is a more modern and flexible alternative. It can accept multiple arguments and also accepts plain strings, which it automatically wraps in text nodes — so you do not need to call document.createTextNode() explicitly.

const section = document.querySelector('#content');

const heading = document.createElement('h2');
heading.textContent = 'Section Title';

const para = document.createElement('p');
para.textContent = 'First paragraph.';

// Append multiple nodes and a raw string in one call
section.append(heading, para, 'Some trailing plain text.');

Both methods trigger an immediate live update to the rendered page. The browser reflows and repaints the affected region right after the JavaScript call completes. For bulk inserts involving many nodes, it is more efficient to build the nodes into a DocumentFragment first and then insert the fragment in a single operation:

const fragment = document.createDocumentFragment();

for (let i = 1; i <= 5; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}

// Only one DOM insertion, one reflow
document.getElementById('my-list').appendChild(fragment);

Inserting Nodes at Specific Positions with insertBefore() and insertAdjacentElement()

Sometimes you need to place a new node somewhere other than the end of a parent's children. insertBefore() and insertAdjacentElement() give you that precision.

parentElement.insertBefore(newNode, referenceNode) inserts newNode directly before referenceNode in the parent's child list. Both the new node and the reference node must share the same parent.

const list = document.getElementById('my-list');
const secondItem = list.children[1]; // the second <li>

const newItem = document.createElement('li');
newItem.textContent = 'Inserted before second item';

list.insertBefore(newItem, secondItem);
// newItem is now between the first and second original items

If referenceNode is null, insertBefore behaves exactly like appendChild and places the new node at the end.

insertAdjacentElement(position, element) is called on an existing target element and offers four positional keywords that give you full control over where the new node lands relative to the target:

Position keyword Where the new node is inserted
'beforebegin' Immediately before the target element itself (as a preceding sibling)
'afterbegin' Inside the target element, before its first child
'beforeend' Inside the target element, after its last child (equivalent to append())
'afterend' Immediately after the target element itself (as a following sibling)
const card = document.querySelector('.card');

const badge = document.createElement('span');
badge.textContent = 'NEW';
badge.className = 'badge';

// Place the badge as the very first child inside the card
card.insertAdjacentElement('afterbegin', badge);

const footer = document.createElement('footer');
footer.textContent = 'Card footer';

// Place a footer immediately after the card in the DOM
card.insertAdjacentElement('afterend', footer);

The sibling-level positions ('beforebegin' and 'afterend') only work when the target element has a parent; calling them on a detached node or on document.body at the top level may produce null. A related method, insertAdjacentHTML(), does the same thing but accepts an HTML string instead of a node — convenient, but again requires caution with untrusted input.

Removing Nodes with removeChild() and remove()

To detach a node from the DOM entirely, JavaScript provides two complementary methods: the older removeChild() and the newer, more convenient remove().

parentElement.removeChild(targetNode) requires you to call the method on the parent of the node you want to remove, and pass the target node as the argument. It returns the removed node, which remains in memory.

const list = document.getElementById('my-list');
const itemToRemove = document.getElementById('item-3');

// Classic pattern: navigate to parent first
list.removeChild(itemToRemove);

// Or navigate to parent dynamically
const staleAlert = document.getElementById('warning-banner');
staleAlert.parentNode.removeChild(staleAlert);

The modern element.remove() method is called directly on the node itself, with no need to reference or navigate to its parent. This makes the code much more concise.

const banner = document.getElementById('cookie-banner');
banner.remove(); // removes itself from the DOM

A subtlety that trips up many developers: removed nodes are not automatically garbage-collected. If a JavaScript variable still holds a reference to the removed node, the node object stays alive in memory. To allow the browser to reclaim that memory, you must explicitly clear the reference:

let tempNode = document.createElement('div');
document.body.appendChild(tempNode);

// Later, remove it from the DOM
tempNode.remove();

// The node still exists in memory because tempNode references it.
// Clear the reference to allow garbage collection:
tempNode = null;

Conversely, you can intentionally keep a reference to a removed node if you plan to re-insert it later — the node retains all its properties, content, and event listeners while held in memory.

Cloning Nodes with cloneNode()

Rather than rebuilding a complex element structure from scratch every time you need a repeated UI component — like a product card, a notification toast, or a table row — you can clone an existing node using cloneNode().

The method accepts a single boolean argument:

  • element.cloneNode(true) — performs a deep clone: the element itself, all of its descendant elements, all text nodes, and all attributes are duplicated.
  • element.cloneNode(false) — performs a shallow clone: only the element's own tag and its attributes are copied; child nodes are not included.
// Original card in the DOM
const originalCard = document.querySelector('.card');

// Deep clone — copies everything inside the card too
const cardCopy = originalCard.cloneNode(true);

// Modify the clone before inserting (to avoid duplicate IDs, etc.)
cardCopy.querySelector('.card__title').textContent = 'Second Card';
cardCopy.id = 'card-2';

// Insert the clone into the page
document.querySelector('#card-grid').appendChild(cardCopy);
// Shallow clone — only the <ul> tag itself, no <li> children
const emptyList = document.querySelector('ul').cloneNode(false);
emptyList.id = 'new-empty-list';
document.body.appendChild(emptyList);

Important caveats with cloning:

  • IDs are duplicated. If the original element has an id, the clone will have the same id, which violates the rule that IDs must be unique in a document. Always update or remove IDs on clones before inserting them.
  • Event listeners attached via addEventListener() are not cloned. The clone gets a copy of the element's HTML attributes (including inline onclick handlers) but not listeners registered through JavaScript. You must re-attach those listeners to the clone manually.
  • Like all newly created nodes, a clone must be explicitly inserted into the DOM with one of the insertion methods before it appears on the page.

Replacing Nodes with replaceChild() and replaceWith()

Sometimes you need to swap one node in the DOM for another. This is common when updating a UI component, swapping an image, or replacing placeholder content with real data. JavaScript provides two methods for this purpose.

parentElement.replaceChild(newNode, oldNode) is the classic approach. It is called on the parent of the node being replaced, and requires explicit references to both the incoming node and the node being swapped out. It returns the removed (old) node.

const container = document.getElementById('profile');
const oldAvatar = document.getElementById('avatar-placeholder');

const newAvatar = document.createElement('img');
newAvatar.src = 'user-photo.jpg';
newAvatar.alt = 'User profile photo';
newAvatar.id = 'avatar-photo';

// Replace the placeholder with the real image
container.replaceChild(newAvatar, oldAvatar);
// oldAvatar is detached from the DOM but still in memory

The modern element.replaceWith(...nodes) method is called directly on the node being replaced and is significantly more flexible. It accepts any number of node arguments or plain strings, replacing the target with all of them in sequence.

const oldHeading = document.querySelector('h1');

const newHeading = document.createElement('h1');
newHeading.textContent = 'Updated Page Title';

// Replace the old h1 with the new one
oldHeading.replaceWith(newHeading);
// replaceWith can insert multiple nodes at once
const placeholder = document.getElementById('content-placeholder');

const title = document.createElement('h2');
title.textContent = 'Article Title';

const body = document.createElement('p');
body.textContent = 'Article body text goes here.';

// Replace the single placeholder with two new nodes
placeholder.replaceWith(title, body);

In both cases, the replaced (old) node is removed from the DOM but remains in memory if a JavaScript variable still holds a reference to it. This means you can store the old node before replacing it and re-insert it later if needed — a pattern useful for toggling between different views without rebuilding nodes from scratch each time.

Here is a consolidated reference comparing all the key methods covered in this topic:

Method Called on Purpose Returns
document.createElement(tag) document Creates a new element node in memory The new element
parent.appendChild(node) Parent element Inserts node as last child The appended node
parent.append(...nodes) Parent element Inserts multiple nodes/strings as last children undefined
parent.insertBefore(new, ref) Parent element Inserts new node before reference child The inserted node
target.insertAdjacentElement(pos, el) Target element Inserts el at a named position relative to target The inserted element
parent.removeChild(node) Parent element Removes a specific child node The removed node
node.remove() The node itself Removes the node from its parent undefined
node.cloneNode(deep) The node itself Creates a deep or shallow copy of the node The cloned node
parent.replaceChild(new, old) Parent element Swaps old child for new node The removed (old) node
node.replaceWith(...nodes) The node itself Replaces the node with one or more nodes/strings undefined
NotesConsider pairing this content with live code exercises where learners build a dynamic list (add, remove, reorder items) to reinforce all methods in a single practical context. The DocumentFragment optimisation for bulk inserts is worth emphasising in any performance-focused follow-up discussion.