JAVASCRIPT / THE BROWSER: DOM, EVENTS, AND STORAGE
Creating, inserting, and removing nodes
Build nodes with createElement, place them with append/prepend/before/after, batch them in a fragment, move or clone them, and detach with remove().
What you will learn
- Create elements with document.createElement, then insert with append or prepend
- Position nodes relative to a sibling using before(), after(), and replaceWith()
- Move an existing node by re-inserting it; use cloneNode(true) to duplicate instead
- Batch many inserts in a DocumentFragment and detach nodes with node.remove()
Understanding Creating, inserting, and removing nodes
document.createElement builds an element that exists only as a JavaScript object: it has a tagName, you can set its properties, and its parentNode is null. Nothing about it reaches the screen until you link it into a node that is itself in the document, which is the job of append, prepend, before, after and the older appendChild and insertBefore. Creation and insertion are two separate acts, and skipping the second one fails silently: no error, no element.
Every node in a tree has at most one parent, and the insertion methods enforce that rule. If the node you pass already has a parent, it is unlinked from that parent before being linked into the new position, so the same append call means 'add' or 'move' depending on where the node currently lives. That is why re-inserting a node is the normal way to reorder a list, and why cloneNode(true) exists: it is the only way to get a second, independent node with the same descendants (cloneNode() without the argument copies just the element shell).
Removal is the same mechanism in reverse. node.remove(), or parent.removeChild(node), unlinks the node and sets its parentNode to null, but the object itself survives as long as your code holds a reference, event listeners included, so you can put it back later. Because each insertion into the live document can force the browser to recompute layout, build subtrees while they are still detached, inside a plain container or a document.createDocumentFragment(), and insert once; a fragment is a parentless holder that dissolves on insertion, moving its children into the target and leaving itself empty.
The methods differ in what they accept and return, and that shapes how you write the code. append, prepend, before and after take any number of arguments and convert strings to text nodes, while appendChild and insertBefore take exactly one Node and return it.
// A created node is a plain object: nothing is on screen yet.
const list = document.createElement('ul');
list.id = 'todo';
const frag = document.createDocumentFragment();
for (const label of ['buy milk', 'write tests', 'walk dog']) {
const li = document.createElement('li');
li.append(label); // append() turns a string into a text node
frag.append(li);
}
list.append(frag); // the fragment's children move into the <ul>
console.log('items built:', list.children.length);
console.log('fragment left behind:', frag.childNodes.length);
list.prepend(list.lastElementChild); // re-inserting moves, it does not copy
console.log('first item:', list.firstElementChild.textContent);
const dropped = list.children[1];
dropped.remove();
console.log('dropped node survives:', dropped.textContent, '| parent:', dropped.parentNode);
document.body.append(list); // one visible insertion, at the very end
console.log('in the page:', document.body.contains(list));
console.log(list.outerHTML);Inserting a node never copies it: a node has one parent, so creating, moving, and removing are all operations on the single object's link into the tree.
Worked examples
Moving versus cloning
Shows that inserting an attached node relocates it, while cloneNode(true) produces a separate object.
const box = document.createElement('div');
const other = document.createElement('div');
for (const t of ['alpha', 'beta']) {
const p = document.createElement('p');
p.append(t);
box.append(p);
}
const first = box.firstElementChild;
other.append(first); // same node, new parent
console.log('box:', box.innerHTML);
console.log('other:', other.innerHTML);
console.log('same object?', other.firstElementChild === first);
const copy = box.firstElementChild.cloneNode(true);
box.append(copy);
console.log('box after clone:', box.innerHTML);
console.log('clone === original?', copy === box.firstElementChild);Example explained
Line 1other.append(first) unlinks the paragraph from box before linking it into other, so box loses it without any removal call.
Line 2The identity check is true because there is still exactly one alpha node in memory, just under a different parent.
Line 3cloneNode(true) builds a new element plus copies of its descendants, so box ends with two beta paragraphs.
Line 4The last check is false because the clone is a distinct object appended after the original, which is still firstElementChild.
Positioning relative to siblings
Compares before(), insertBefore(), and replaceWith() for placing a node at an exact spot.
const nav = document.createElement('nav');
const home = document.createElement('a'); home.append('home');
const help = document.createElement('a'); help.append('help');
nav.append(home, help); // several nodes in one call
const docs = document.createElement('a'); docs.append('docs');
help.before(docs);
console.log(nav.innerHTML);
const settings = document.createElement('a'); settings.append('settings');
nav.insertBefore(settings, nav.firstChild);
console.log(nav.innerHTML);
const logout = document.createElement('a'); logout.append('logout');
help.replaceWith(logout);
console.log(nav.innerHTML);
console.log('help detached:', help.parentNode);Example explained
Line 1nav.append(home, help) inserts two nodes at once, which appendChild cannot do.
Line 2help.before(docs) is called on the sibling, so you never compute an index for the parent.
Line 3nav.insertBefore(settings, nav.firstChild) is the older equivalent of prepend, and the reference node must already be a child of nav.
Line 4replaceWith links logout into help's exact slot and unlinks help, which is why help.parentNode is null.
Removing over a live collection
Demonstrates why an index loop over element.children skips nodes during removal and how a snapshot fixes it.
function makeSpans() {
const box = document.createElement('div');
for (const n of [1, 2, 3, 4]) {
const s = document.createElement('span');
s.append(String(n));
box.append(s);
}
return box;
}
const a = makeSpans();
for (let i = 0; i < a.children.length; i++) {
a.children[i].remove();
}
console.log('naive loop left:', a.innerHTML);
const b = makeSpans();
for (const el of [...b.children]) el.remove();
console.log('snapshot loop left:', JSON.stringify(b.innerHTML));Example explained
Line 1a.children is a live collection, so removing index 0 immediately shifts span 2 down into index 0.
Line 2The counter has already moved to 1, so the loop hits spans 1 and 3 and then stops when length falls to 2.
Line 3[...b.children] copies the node references into an array, so later removals cannot change what the loop walks.
Line 4JSON.stringify makes the empty string that innerHTML returns visible instead of printing a blank line.
Important notes
append, prepend, before, after and remove return undefined, while appendChild and insertBefore return the inserted node, so chaining off append throws a TypeError.
insertBefore(newNode, ref) requires ref to be a child of the node you call it on; an unrelated reference node raises NotFoundError instead of appending.
Common mistakes
Creating the element and filling it in but never inserting it into an attached node: the page does not change and no error appears, so it looks like createElement failed.
Passing text to appendChild, as in li.appendChild('milk'), which throws "Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'"; append('milk') or document.createTextNode('milk') is what handles strings.
Reusing one created node inside a loop over several parents expecting a copy in each: it is moved every time, so only the last parent ends up with it and the others stay empty.
Try it yourself
Change, predict, then run
In a blank page, use a DocumentFragment to build a <ul> with five <li> items and insert it into document.body with a single call. Then move the last item to the front, remove the middle one, and log list.outerHTML after each change to confirm the item count only ever drops by one.
Open the JavaScript workspaceCheck your understanding
listA already contains three <li> elements. You loop over those three nodes and call listB.append(li) on each. What is the state of the two lists afterwards?
- Both lists end up with the three items, because append() copies the node into the new parent.
- listB ends up with only the last item, because each append() replaces the one before it.
- listB has all three items and listA is empty, because inserting an attached node unlinks it from its old parent first.
- The items stay in listA until you also call listA.removeChild(li) for each one, and only then appear in listB.
Show answer
A node can have only one parent, so each append unlinks the <li> from listA before linking it into listB, leaving listA empty. Option 0 is tempting because reading nodes out of listA feels non-destructive, but only cloneNode(true) creates a second node; option 3's explicit removeChild is redundant since insertion already detaches.