Creating & changing elements
Build, insert, move, clone, replace, and remove DOM elements with createElement, append, DocumentFragment, template, and insertAdjacentHTML.
- 01Build nodes safelyCreate elements, set text or attributes, and attach them only when ready.
- 02Choose the right insertion methodUse append, prepend, before, after, replaceWith, remove, insertBefore, and insertAdjacentHTML with confidence.
- 03Avoid DOM surprisesExplain moving versus cloning, fragments emptying, inert templates, listener cloning, and HTML-injection risk.
Build first, insert second
The previous DOM lessons taught you how to find existing nodes and read their text, HTML, attributes, and properties. This lesson is where the page starts changing shape. You will create brand-new elements, place them around existing ones, move them between parents, clone them, remove them, batch them in fragments, stamp them from templates, and insert trusted HTML strings at precise positions.
The most important mental model is simple: creating a node is not the same as inserting it. A new element can be configured safely while it is still off the page. Only an insertion method makes it part of the document tree that visitors see.
To modify the DOM, create or find nodes, change their text or attributes, then insert, move, replace, or remove those nodes in the document tree.
A loose LEGO brick is real before it is snapped onto the board. You can choose its color, add a sticker, and connect smaller bricks to it while it sits in your hand. A DOM element works the same way: create it, prepare it, then attach it when the piece is ready.
- In real life: Taking a new brick out of the box
- In JavaScript:
document.createElement()creates a loose element - In real life: Writing a label on the brick
- In JavaScript:
textContent, properties, and attributes configure it - In real life: Snapping the brick onto the board
- In JavaScript:
append,prepend,before, orafterinserts it - In real life: Moving a brick already on the board
- In JavaScript: Inserting an existing node moves it to the new place
- In real life: Photocopying a brick
- In JavaScript:
cloneNode(true)copies the element and descendants
Where the analogy stops: Real LEGO bricks cannot accidentally keep duplicate ID labels, and photocopies do not have event listeners. In the DOM, deep clones copy attributes but not listeners added with addEventListener, so you must fix IDs and wire behavior yourself.
In the experiments below, every live DOM change happens inside an empty React ref container. React creates the empty box; the DOM APIs fill it. That keeps the demos safe next to React and mirrors how you might progressively enhance a small part of a page.
createElement and safe text
STEP THROUGHdocument.createElement("article") returns a new HTMLElement. It has no parent yet, so it is not visible. That off-page moment is useful: you can fill text with textContent, set attributes, append children, and only then insert the finished piece.
Step through a tiny model of DOM construction. The recording is a pure model so it can render on the server, and the live page verifies the same operations in your browser.
script
const title = createElement("h3");title.textContent = "DOM field note";card.setAttribute("class", "note-card");card.append(title);console.log(card.outerHTML);Notice that the replay is a small model, not a paused browser debugger. It records the same create → fill → attach order that the browser runs. The live playgrounds that follow verify the DOM behavior in your real browser.
| Operation | Parses HTML? | Use it for |
|---|---|---|
element.textContent = value | No | User text, labels, messages, data from APIs |
element.setAttribute(name, value) | No | HTML attributes such as aria-label, data-id, or class |
element.append(child) | Strings become Text nodes | Adding child nodes or safe text after existing children |
element.innerHTML = html | Yes | Only trusted, intentional markup; not user input |
If a string is plain text, use textContent. If a string is meant to be markup, treat it as dangerous until you know where it came from and whether it was sanitized.
append, prepend, before, and after
INTERACTIVEInsertion methods answer two questions: inside or outside? and start or end? append and prepend insert children inside a parent. before and after insert siblings around the target itself.
const list = document.querySelector("#planets");const target = list.querySelector(".target");const badge = document.createElement("li");badge.textContent = "New moon"; list.append(badge);list.prepend(badge);target.before(badge);target.after(badge);target.replaceWith(badge);list.insertAdjacentHTML("beforeend", "<li>HTML comet</li>");Reset builds the list in an empty ref container, then the buttons run real DOM methods on nodes React is not managing.
The modern methods are flexible: append, prepend, before, after, and replaceWith accept any number of nodes and strings. Strings become Text nodes. They are never parsed as HTML. The older appendChild accepts one Node and returns it; the modern append returns undefined. The older insertBefore(newNode, referenceNode) inserts before a reference child, and referenceNode === null means append at the end.
Moving, cloning, replacing, and removing
INTERACTIVEA node has one parent. If it is already in the document and you insert it somewhere else, the browser moves it. Nothing is copied unless you ask for a clone.
const inbox = document.querySelector("#inbox");const done = document.querySelector("#done");const task = inbox.querySelector("#task-1"); task.addEventListener("click", () => log("original clicked")); // Move: the original leaves inbox and enters done.done.append(task);const shallow = task.cloneNode(false);const deep = task.cloneNode(true);Move the original, then add clones and click them. Only the original has the JavaScript-added listener.
| Goal | Method | Professional note |
|---|---|---|
| Move one existing node | newParent.append(node) | The node leaves its previous parent first. |
| Copy just one element | node.cloneNode(false) | Attributes copy; children do not. |
| Copy a subtree | node.cloneNode(true) | Descendants and attributes copy; addEventListener listeners do not. |
| Swap a node | oldNode.replaceWith(newNode, "text") | Accepts nodes and strings, like before and after. |
| Detach a node | node.remove() | Returns undefined; parent.removeChild(child) returns the child. |
Deep cloning has two surprises. First, listeners registered with addEventListener do not come along. Second, attributes do copy, including id. If you clone a card with id="profile-card", you now have duplicate IDs unless you fix them.
DocumentFragment and <template>
INTERACTIVEA DocumentFragment is a lightweight container for nodes. You can assemble many children in it, then append the fragment once. The fragment itself does not become a wrapper element. Its children move into the destination, leaving the fragment empty.
Imagine building a model on a tray, then sliding the finished pieces onto the table. The tray helped you carry the batch, but it is not part of the model.
- In real life: A tray holding 100 little parts
- In JavaScript: A
DocumentFragmentholding 100linodes - In real life: Slide the parts onto the table
- In JavaScript: Append the fragment to the real list
- In real life: The tray is empty afterward
- In JavaScript:
fragment.childNodes.lengthbecomes0
Where the analogy stops: A physical tray might stay under the parts. A DocumentFragment does not remain as a visible wrapper in the DOM.
A cookie cutter waits until you press it into dough. A <template> also waits without activating its contents. Clone the content, fill in safe text, and insert it.
- In real life: A cutter waits in the kitchen drawer
- In JavaScript: A
<template>stays inactive - In real life: Press out a fresh cookie shape
- In JavaScript:
template.content.cloneNode(true) - In real life: Add icing to each cookie
- In JavaScript: Set text on the cloned nodes
Where the analogy stops: A cookie cutter is not copied. In the DOM, clone template content for every new instance.
const fragment = document.createDocumentFragment();for (let i = 1; i <= 100; i += 1) { const item = document.createElement("li"); item.textContent = "Row " + i; fragment.append(item);}list.append(fragment); // fragment.childNodes.length is now 0 const copy = template.content.cloneNode(true);copy.querySelector("h3").textContent = name;cards.append(copy);Build the 100-row fragment, then stamp template cards. Fragment count: not built.
insertAdjacentHTML and friends
INTERACTIVESometimes you intentionally have a trusted HTML string, such as markup generated by your own code. insertAdjacentHTML(position, html) parses that string at one of four positions: beforebegin, afterbegin, beforeend, or afterend. It does not replace existing siblings or children outside the insertion point.
const panel = document.querySelector(".panel");const input = panel.querySelector("input");input.value = "typed text"; // Safer for trusted markup: does not replace existing nodes.panel.insertAdjacentHTML("beforeend", "<p>Added</p>");// Avoid this pattern for adding: it reparses children.panel.innerHTML += "<p>Added</p>";Type in the input, then try each insertion method.
Do not pass untrusted strings to innerHTML, outerHTML, or insertAdjacentHTML. A string can contain event handler attributes, dangerous URLs, or script-like behavior. Prefer textContent for data, or sanitize on a trusted path before parsing.
There are two related methods with the same four positions: insertAdjacentElement(position, element) inserts an element node, and insertAdjacentText(position, text) inserts text without parsing HTML. To clear or replace all children in one readable operation, use replaceChildren().
append("<b>hi</b>")textContent = "<b>hi</b>"appendChild(node)target.before("label", icon)insertAdjacentHTML("beforeend", html)innerHTML += htmlold.replaceWith(newNode, "saved")node.remove()
Sort each method by what it accepts or does. Watch especially for strings: most insertion methods treat them as text, not markup.
container.append("<b>hi</b>");
container.appendChild(document.createElement("b"));
target.before("label", icon);
list.insertBefore(item, null);
panel.insertAdjacentHTML("beforeend", "<b>hi</b>");
box.textContent = "<b>hi</b>";
oldCard.replaceWith(newCard, " saved");Where you will use this
You will use these APIs any time a page needs to render data without a full refresh: search results, todo lists, validation messages, notifications, empty states, and small widgets on server-rendered pages. Frameworks like React usually generate and update DOM nodes for you, but knowing the underlying operations makes debugging much easier.
function renderTodos(todos) {
const list = document.createElement("ul");
for (const todo of todos) {
const item = document.createElement("li");
item.textContent = todo.title;
if (todo.done) item.setAttribute("data-done", "true");
list.append(item);
}
app.replaceChildren(list);
}This function creates a fresh list, fills each li with safe text, then uses replaceChildren to swap the app’s children in one step. If the list had old loading text, it disappears. If no arguments are passed, app.replaceChildren() simply clears the container.
Element.moveBefore() is a newer state-preserving move API in some browsers. It is not everywhere, so feature-detect it before use. The methods in this lesson are the standard baseline you will see most often.
Common misconceptions
- “createElement shows it immediately.” No. Creation makes a loose node. Insertion makes it visible.
- “append copies existing nodes.” No. Existing nodes move because a node has one parent.
- “append parses HTML strings.” No. Strings become Text nodes. Use HTML parsing APIs only for trusted markup.
- “cloneNode(true) copies everything.” It copies attributes and descendants, not listeners added with
addEventListeneror most JavaScript state. - “A fragment remains as a wrapper.” No. Inserting a fragment moves its children and leaves it empty.
- “innerHTML += is just append for HTML.” It reparses and replaces children, which can disturb input state and listeners.
Practice exercises
5 EXERCISESCreate an li, give it safe text, and insert it into a list.
const item = document.createElement("li");
// Add the text "Read DOM lesson" safely.
// Insert it at the end of list.const item = document.createElement("li");
item.textContent = "Read DOM lesson";
list.append(item);The element is created first, filled with safe text, then appended as the list’s last child.
Predict the logged type.
const fakeElement = {
append(...items) {
console.log(typeof items[0]);
},
};
fakeElement.append("<b>hi</b>");It prints string. In the real DOM that string would become a Text node containing <b>hi</b>.
Use this pure model to predict the count after a fragment insertion.
const fragment = { childNodes: ["a", "b", "c"] };
const list = [];
list.push(...fragment.childNodes);
fragment.childNodes.length = 0;
console.log(list.length + ":" + fragment.childNodes.length);It prints 3:0. A real DocumentFragment behaves the same way after insertion: destination has the children, fragment has none.
Predict the output of this moving model.
const inbox = ["A", "B"];
const done = [];
const item = inbox.splice(inbox.indexOf("B"), 1)[0];
done.push(item);
console.log(inbox.join("") + "|" + done.join(""));It prints A|B: B moved from the first list to the second. DOM nodes move the same way when inserted elsewhere.
Write the body of renderMessage. Which method clears all old children?
function renderMessage(container, message) {
const p = document.createElement("p");
// Fill p safely, then replace all old children.
}function renderMessage(container, message) {
const p = document.createElement("p");
p.textContent = message;
container.replaceChildren(p);
}textContent treats message as text, and replaceChildren(p) removes any previous loading or error content before inserting the new paragraph.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does
document.createElement("li")do immediately?Choose an answer to see the explanation.
Question 2 of 8What happens when you append a node that is already in another list?
Choose an answer to see the explanation.
Question 3 of 8Which call parses the string as HTML?
Choose an answer to see the explanation.
Question 4 of 8What does this print in the model from the exercises?
Read the code, then predictconst inbox = ["A", "B"]; const done = []; const item = inbox.splice(inbox.indexOf("B"), 1)[0]; done.push(item); console.log(inbox.join("") + "|" + done.join(""));Choose an answer to see the explanation.
Question 5 of 8Which statement about
cloneNode(true)is correct?Choose an answer to see the explanation.
Question 6 of 8After you append a
DocumentFragment, what is true about the fragment?Choose an answer to see the explanation.
Question 7 of 8Which
insertAdjacentHTMLposition inserts as the first child of the target element?Choose an answer to see the explanation.
Question 8 of 8Which method replaces all children of an element when you pass new children, or clears it with no arguments?
Choose an answer to see the explanation.
Key takeaways
createElementcreates a loose element; insertion methods attach it to the document.append,prepend,before,after, andreplaceWithaccept nodes and strings; strings become Text nodes.- Inserting an existing node moves it. Use
cloneNodefor copies, then fix duplicate IDs and reattach JavaScript listeners. DocumentFragmentbatches children and empties when inserted.template.contentis an inert fragment you clone.insertAdjacentHTMLparses trusted HTML at a precise position. UsetextContentfor untrusted text.
Final definition: DOM modification is the art of creating or finding nodes, filling them safely, and deliberately moving, copying, replacing, or removing them in the document tree.
Up next: Styles & classes.