Creating & changing elements
Build, insert, move, clone, replace, and remove DOM elements with createElement, append, DocumentFragment, template, and insertAdjacentHTML.
- 01Build elements safelyMake a new element, give it text and attributes, and put it on the page only when it is ready.
- 02Put elements in the right placePick the right method: append, prepend, before, after, replaceWith, remove, insertBefore, or insertAdjacentHTML.
- 03Avoid surprisesExplain why a node moves instead of being copied, why a fragment ends up empty, what a copy leaves behind, and why an HTML string can be dangerous.
Build first, insert second
The DOM is the tree of objects the browser builds from a page's HTML. Each part of the tree is a node: an element such as a paragraph, or a piece of text. In the last few lessons you found nodes and read their text and attributes. In this lesson you change the page.
You will learn to:
- make new elements;
- put them on the page, in the exact spot you want;
- move, copy, replace, and remove them;
- build many at once, with a fragment or a template;
- add HTML from a string, and know when that is not safe.
One rule matters most: making an element does not put it on the page. A new element sits on its own until you insert it. While it waits, you can give it text and attributes. Then one method call puts it on the page.
To change the page, you make or find a node, set its text or attributes, and then insert it, move it, replace it, or remove it.
A LEGO brick in your hand is a real brick, even before it is on the board. You can put a sticker on it. You can join smaller bricks to it. When it is ready, you snap it onto the board. A new element works the same way: make it, fill it in, then put it on the page.
- In real life: Take a new brick out of the box
- In JavaScript:
document.createElement()makes a new element, not on the page yet - In real life: Put a sticker on the brick
- In JavaScript:
textContentand attributes fill in the element - In real life: Snap the brick onto the board
- In JavaScript:
append,prepend,before, orafterputs it on the page - In real life: Pull a brick off and snap it somewhere else
- In JavaScript: Inserting an element that is already on the page moves it
- In real life: Make a second brick just like it
- In JavaScript:
cloneNode(true)copies the element and everything inside it
The experiments below change a real page. Each one works inside an empty box on this page. React, the tool that builds this site, makes the empty box. The DOM methods from this lesson fill it. That way your changes never break the rest of the page.
Make an element with createElement
STEP THROUGHdocument.createElement("article") makes a new <article> element and gives it back to you. The new element has no parent yet. A parent is the element that holds another element. With no parent, the element is not on the page, so nobody can see it.
That is a good moment to get it ready. You can:
- set its text with
textContent; - set attributes, such as
class; - put other elements inside it.
Then you insert the finished element. Step through the code below to see each step. Here createElement is a short name for document.createElement.
Step through building one card. This replay is a small copy of what the browser does, so it can show each step. The experiments below run the real methods 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);This replay is a small copy of what the browser does. It is not the browser itself. It shows the same order: make, fill, then join. The experiments later in this lesson run the real methods in your browser.
Text can be a node of its own, too. The next example makes one on purpose.
const item = document.createElement("li");
const label = document.createTextNode("Buy tea");
item.appendChild(label);
list.appendChild(item);
console.log(item.textContent);- Line 1 makes an empty list item,
item. - Line 2 makes a text node: a node that holds only the text
Buy tea. - Line 3 puts the text node inside
itemwithappendChild. - Line 4 puts
itemat the end of a list that is already on the page. - Line 5 prints
Buy tea.
appendChild takes exactly one node. It gives back the node it added. It never reads a string as HTML.
li is ready, but it is not on the page yet. Line 4, list.appendChild(item), puts it into the list.Each row of this table is one way to fill a new element. The middle column says whether the browser reads tags such as <b> in your string as real HTML.
| What you write | Does it read tags as HTML? | Use it for |
|---|---|---|
element.textContent = value | No. Tags show up as plain characters. | Any text: names, messages, data from a server, words a user typed. |
element.setAttribute(name, value) | No. It sets one attribute. | Attributes such as aria-label, data-id, or class. |
element.append(child) | No. A string becomes a text node. | Adding elements, or safe text, after the children already there. |
element.innerHTML = html | Yes. Tags become real elements. | Only HTML you wrote yourself. Never words a user typed. |
If a string is plain text, use textContent. If a string is meant to be HTML, treat it as dangerous. First find out where it came from. Only use it if you trust that source, or if it was sanitized, which means cleaned of anything harmful.
append, prepend, before, and after
INTERACTIVEFour methods put a node next to a target, the element you call the method on. Each method answers two questions: inside the target or outside it? At the start or at the end?
appendputs the node inside the target, as its last child.prependputs the node inside the target, as its first child.beforeputs the node outside, just before the target.afterputs the node outside, just after the target.
A child is a node inside another node. A sibling is a node next to it, with the same parent. So append and prepend add children, and before and after add siblings. Try each button below and watch where the new item lands.
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 a list of three planets. Each button then runs one real DOM method on that list.
These methods are flexible. append, prepend, before, after, and replaceWith all take as many nodes and strings as you like. Each string becomes a text node. It is never read as HTML.
Two older methods do similar jobs:
appendChild(node)takes exactly one node and gives it back. The newerappendgives backundefined.insertBefore(newNode, referenceNode)putsnewNodejust before the childreferenceNode. IfreferenceNodeisnull, it puts the new node at the end.
What if the node is already on the page, in another list? The next example tries it.
const inbox = document.createElement("ul");
const done = document.createElement("ul");
const task = document.createElement("li");
task.textContent = "Email Sam";
inbox.appendChild(task);
done.appendChild(task);
console.log(inbox.children.length + "|" + done.textContent);Line 5 puts the task into inbox. Line 6 puts the same task into done. The task does not get copied. It moves, so inbox is left with 0 children. The code prints 0|Email Sam.
task into done. It leaves inbox, so the code prints 0|Email Sam.If both lists need the task, make a copy with cloneNode. You will see how in the next section.
Move, copy, replace, and remove
INTERACTIVEA node can have only one parent. Say a node is already on the page, and you insert it somewhere else. The browser takes it out of its old place and puts it in the new one. That is a move. Nothing gets copied unless you ask for a copy.
To make a copy, call cloneNode. A copy is also called a clone. In the experiment below, a task has a click listener: a function that runs when you click it. You add a listener with addEventListener. Move the task, make copies, and click each one.
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 task, then make copies and click them. Only the original has the listener.
Each row of this table is one job. Find the job you want, then read which method does it.
| What you want | What you write | What to know |
|---|---|---|
| Move a node that is already on the page. | newParent.append(node) | The node leaves its old parent first. |
| Copy one element, without what is inside it. | node.cloneNode(false) | Attributes are copied. Children are not. |
| Copy an element and everything inside it. | node.cloneNode(true) | Attributes and children are copied. Listeners from addEventListener are not. |
| Swap a node for something else. | oldNode.replaceWith(newNode, "text") | Takes nodes and strings, like before and after. |
| Take a node off the page. | node.remove() | Gives back undefined. The older parent.removeChild(child) gives back the child. |
A deep copy, made with cloneNode(true), has two surprises:
- Listeners added with
addEventListenerare not copied. - Attributes are copied, and that includes
id. Anidshould be used only once on a page. If you copy a card withid="profile-card", two elements now have the sameid. Change the copy'sidbefore you use it.
const oldMessage = document.createElement("p");
oldMessage.textContent = "Loading...";
app.replaceChildren(oldMessage);
const readyMessage = document.createElement("p");
readyMessage.textContent = "Ready";
oldMessage.replaceWith(readyMessage);
readyMessage.remove();
console.log(app.children.length);- Line 3 uses
replaceChildrento put theLoading...paragraph insideapp. - Line 6 uses
replaceWithto swap that paragraph for theReadyparagraph. - Line 7 uses
removeto take theReadyparagraph off the page. - Line 8 prints
0, becauseapphas no children left.
These methods really change the tree. They do not just hide an element with CSS.
const original = document.createElement("article");
original.id = "profile";
const name = document.createElement("h2");
name.textContent = "Asha";
original.append(name);
original.addEventListener("click", () => console.log("clicked"));
const copy = original.cloneNode(true);
copy.id = "profile-copy";
console.log(copy.id + "|" + copy.textContent + "|" + (copy.children.length === 1));- Lines 1 to 5 make an
articlewith the idprofileand a heading,Asha. - Line 6 adds a click listener to the original.
- Line 7 makes a deep copy. The copy gets the heading and the id
profile. - Line 8 changes the copy's id to
profile-copy, so the id is not used twice. - Line 9 prints
profile-copy|Asha|true.
The copy did not get the click listener from line 6. If the copy needs to react to clicks, add a new listener to it.
Build many at once: DocumentFragment and <template>
INTERACTIVEA DocumentFragment is a light holder for nodes. It is never shown on the page. You use it like this:
- Make a fragment with
document.createDocumentFragment(). - Put many nodes into the fragment, one by one.
- Append the fragment to the page, once.
The fragment itself is not added to the page. Only its children are. They move out of the fragment, so the fragment is empty afterwards.
You put the parts of a model on a tray. Then you slide them all onto the table at once. The tray helped you carry the parts. It does not become part of the model.
- In real life: A tray holding 100 small 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 afterwards
- In JavaScript:
fragment.childNodes.lengthbecomes0
A <template> element holds HTML that the browser does not use yet. Images inside it do not load. Scripts inside it do not run. Its content property is a fragment with that HTML. You copy the content with cloneNode(true), fill in the copy, and insert it. You can do that as many times as you like.
A cookie cutter does nothing until you press it into dough. Each press gives you a new cookie of the same shape. Then you decorate each cookie in its own way.
- In real life: The cutter waits in the kitchen drawer
- In JavaScript: A
<template>waits and does nothing - In real life: Press out a new cookie
- In JavaScript:
template.content.cloneNode(true) - In real life: Put icing on each cookie
- In JavaScript: Set the text on the copied nodes
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 100 rows with a fragment, then make cards from the template. Rows in the fragment: not built.
const fragment = document.createDocumentFragment();
for (const title of ["Tea", "Bread"]) {
const item = document.createElement("li");
item.textContent = title;
fragment.append(item);
}
list.append(fragment);
const card = template.content.cloneNode(true);
card.querySelector("h3").textContent = "Asha";
cards.append(card);
console.log(list.children.length + "|" + cards.querySelector("h3").textContent);- Line 1 makes an empty fragment.
- Lines 2 to 6 make two list items,
TeaandBread, and put them in the fragment. - Line 7 appends the fragment. Both items move into
list, and the fragment is left empty. - Line 8 copies the template's content.
- Line 9 sets the copied heading's text to
Asha. - Line 10 puts the copy into
cards. - Line 11 prints
2|Asha.
list. The fragment itself is not added, and it is empty afterwards.When should you use each one?
- Use a fragment when several new nodes belong to one change.
- Use a template when you add the same shape again and again, like one card per user.
Neither one makes text safe by itself. For text, still use textContent.
Add HTML from a string with insertAdjacentHTML
INTERACTIVESometimes you do want a string read as HTML. For example, your own code wrote the string, so you trust it. insertAdjacentHTML(position, html) reads the string as HTML and adds the new elements at one of four positions:
beforebegin: outside, just before the element.afterbegin: inside, before its first child.beforeend: inside, after its last child.afterend: outside, just after the element.
It only adds. It leaves every node that is already there alone. Type in the box below, then compare it with innerHTML +=.
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 box, then try each button.
const button = document.createElement("button");
let clicks = 0;
button.addEventListener("click", () => { clicks += 1; });
panel.append(button);
panel.innerHTML += "<p>Added</p>";
panel.querySelector("button").click();
console.log(clicks);- Line 3 adds a listener that adds 1 to
clickseach time the button is clicked. - Line 4 puts the button inside
panel. - Line 5 runs
panel.innerHTML += "<p>Added</p>". This turns all of the panel's HTML into a string, adds the new part, and reads the whole string again. The browser throws away the old button and makes a new one. The new button has no listener. - Line 6 clicks the new button. Nothing happens.
- Line 7 prints
0.
To add HTML you trust without rebuilding what is already there, use insertAdjacentHTML.
Never pass a string you don't trust to innerHTML, outerHTML, or insertAdjacentHTML. A string like that can hold an attribute such as onerror that runs code, or a dangerous link. This kind of attack is called XSS, short for cross-site scripting. For text, use textContent. For HTML from somewhere else, sanitize it first.
Two more methods use the same four positions:
insertAdjacentElement(position, element)puts an element there.insertAdjacentText(position, text)puts text there. It never reads tags.
To remove all of an element's children, or swap them all for new ones in one step, use replaceChildren().
const message = document.createElement("p");
const userText = "<strong>Welcome</strong>";
message.textContent = userText;
app.append(message);
console.log(message.children.length + "|" + message.textContent);The string on line 2 has tags in it. Line 3 puts it into the paragraph with textContent, which never reads tags. So the paragraph has no child elements. The page shows the angle brackets as plain characters. Line 5 prints 0|<strong>Welcome</strong>.
textContent never reads tags. The page shows the angle brackets as plain characters, and the paragraph has no child elements.append("<b>hi</b>")textContent = "<b>hi</b>"appendChild(node)target.before("label", icon)insertAdjacentHTML("beforeend", html)innerHTML += htmlold.replaceWith(newNode, "saved")node.remove()
Put each call in the group that says what it does. Look closely at strings: most of these methods show a string as plain text, not HTML.
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");const note = document.createElement("p");
note.textContent = "Saved";
panel.insertAdjacentElement("beforebegin", note);
panel.insertAdjacentText("afterend", "End of panel");
console.log(note.textContent + "|" + panel.nextSibling.textContent);- Lines 1 and 2 make a paragraph that says
Saved. - Line 3 puts the paragraph just before the panel, outside it.
- Line 4 puts the text
End of paneljust after the panel, outside it. - Line 5 prints
Saved|End of panel.
insertAdjacentElement takes an element node. insertAdjacentText makes a text node and never reads tags.
Where you will use this
You use these methods whenever a page shows new things without loading a whole new page. For example:
- search results;
- to-do lists;
- error messages under a form field;
- pop-up notices;
- a "nothing here yet" message when a list is empty.
Tools like React usually make and change DOM nodes for you. Knowing these methods still helps you understand what the tool does, and find bugs faster.
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);
}- Line 2 makes a new, empty list.
- Lines 4 to 9 make one
lifor each to-do, set its text withtextContent, and add it to the list. - Line 11 uses
replaceChildrento swap everything insideappfor the new list, in one step. Any old text, like a loading message, is gone.
If you call app.replaceChildren() with nothing in the brackets, it just empties app.
const list = document.createElement("ul");
function addTodo(title) {
const item = document.createElement("li");
item.textContent = title;
list.append(item);
return item;
}
addTodo("Read DOM lesson");
const extra = addTodo("Try cloneNode");
extra.remove();
app.replaceChildren(list);
console.log(list.textContent.trim());- Lines 2 to 7 define
addTodo. It makes one item, sets its text, adds it to the list, and gives the item back. - Line 8 adds
Read DOM lesson. - Line 9 adds
Try cloneNodeand keeps the item inextra. - Line 10 removes that second item.
- Line 11 puts the finished list into
app. - Line 12 prints
Read DOM lesson.
This works well for a small part of a page. But if a tool like React looks after that same part, let the tool make the changes. Two things changing the same nodes will get in each other's way.
Some browsers have a newer method, Element.moveBefore(). It moves an element and keeps its state, such as the text typed into a box. Not every browser has it yet, so check that it exists before you use it. The methods in this lesson work in every browser.
Common misconceptions
- "
createElementputs the element on the page." No. It only makes the element. It shows up only after you insert it. - "
appendcopies a node that is already on the page." No. The node moves, because a node can have only one parent. - "
appendreads a string as HTML." No. A string becomes a text node. Use the HTML methods only for HTML you trust. - "
cloneNode(true)copies everything." No. It copies attributes and children. It does not copy listeners added withaddEventListener. - "A fragment stays on the page around its children." No. Inserting a fragment moves its children out and leaves it empty.
- "
innerHTML +=just adds HTML at the end." No. It rebuilds all the children. Typed text and listeners on them can be lost.
Practice exercises
5 EXERCISESMake an li, give it safe text, and put it at the end of 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);Line 1 makes the item. Line 2 sets its text safely. Line 3 puts it at the end of the list, as its last child.
This fakeElement is a plain object, not a real element. Its append only prints the type of the first thing it gets. What does it print?
const fakeElement = {
append(...items) {
console.log(typeof items[0]);
},
};
fakeElement.append("<b>hi</b>");It prints string. On a real page, append would turn that string into a text node. The page would show the characters <b>hi</b>, not bold text.
This code uses plain arrays to act out a fragment being appended. What does it print?
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 ends the same way after you append it: the list has the children, and the fragment has none.
This code uses arrays to act out a move. What does it print?
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 left the first list and went into the second. A node on the page moves the same way when you insert it somewhere else.
Finish renderMessage. It should show message in a paragraph, and remove anything that was in container before. Which method removes all the 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 keeps message as plain text. replaceChildren(p) removes any old loading or error message, then puts in the new paragraph.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does
document.createElement("li")do right away?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 reads the string as HTML?
Choose an answer to see the explanation.
Question 4 of 8What does this code from the exercises print?
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 sentence 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 adds the new HTML as the first child of the element?Choose an answer to see the explanation.
Question 8 of 8Which method swaps all of an element's children for new ones, or removes them all when you pass nothing?
Choose an answer to see the explanation.
Key takeaways
createElementmakes an element. It is not on the page until you insert it.append,prepend,before,after, andreplaceWithtake nodes and strings. A string becomes a text node.- Inserting a node that is already on the page moves it. To copy it, use
cloneNode. Then change any copiedid, and add listeners again. - A
DocumentFragmentholds many nodes, and is empty after you append it.template.contentis a fragment you copy each time you need it. insertAdjacentHTMLreads a string as HTML, at one of four positions. Use it only for HTML you trust. For text, usetextContent.
In one sentence: to change the page, you make or find nodes, fill them with safe text, and then move, copy, replace, or remove them on purpose.
Up next: Styles & classes.