Creating, removing and traversing
Build nodes and fragments efficiently, detach and replace them correctly, and walk the tree with the element-only navigation properties.
Creating and inserting
const li = document.createElement('li');
li.className = 'task';
li.textContent = label;
// build in a fragment, insert once
const frag = document.createDocumentFragment();
items.forEach(item => frag.append(makeRow(item)));
list.append(frag); // one insertion, one style recalculation
list.prepend(li); // first child
divider.before(li); // sibling-level insert
ref.insertAdjacentElement('beforebegin', node);appendandprependaccept strings as well as nodes and take several arguments;appendChildtakes one node and returns it.- A
DocumentFragmentis an invisible container: appending it moves its children into the tree and leaves the fragment empty. replaceChildren()empties a node and inserts a new set in a single operation.- Keep one function that builds a row and reuse it; duplicating markup in two places is how the row you append drifts from the row you render on the server.
Removing, replacing and cloning
node.remove(); // detach from the tree
oldNode.replaceWith(newNode);
parent.replaceChildren(); // empty it fast
// cloning copies attributes, text and children - not listeners, not form state
const fresh = template.cloneNode(true);
fresh.querySelector('input').value = ''; // reset what the clone inherited| Task | Modern call | Older equivalent |
|---|---|---|
| Insert at the end | parent.append(node) | appendChild(node) |
| Insert at the start | parent.prepend(node) | insertBefore(node, firstChild) |
| Replace a node | old.replaceWith(node) | replaceChild(node, old) |
| Remove a node | node.remove() | parent.removeChild(node) |
| Empty a node | parent.replaceChildren() | innerHTML = '' |
💡
A detached node still exists in memory with its listeners attached, so you can remove an element and insert it somewhere else later.
cloneNode(true) is different: it produces new objects with no listeners and no value state.Traversing the tree
el.parentElement; // null at <html>
el.children; // elements only (live)
el.childNodes; // includes text and comment nodes
el.firstElementChild;
el.lastElementChild;
el.nextElementSibling;
el.previousElementSibling;
// filter one level without walking whitespace nodes
const done = [...list.children].filter(li => li.matches('.done'));- The
*Element*variants skip text nodes, so whitespace between tags cannot break your walk. childrenis live; spread it or copy it withArray.frombefore removing anything inside the loop.- Go upwards with
closest(), downwards withquerySelectorAll(), and sideways with the sibling properties. childNodes.lengthcounts text and comment nodes, which is why it rarely matches the number of tags you can see.
FAQ
Why did the cloned element lose its event listener?
Listeners live on the node object, and cloning creates new objects without them. Either re-attach the handler after cloning, or attach one handler to a parent and use event delegation.
Should I build HTML strings or DOM nodes?
Strings are shorter for trusted markup but give you nothing to bind to, and they invite injection when user data is interpolated. Nodes and fragments are the safer default for anything the user can influence.
Related
Reading and updating content safely Events
Last refreshed 2026-09-18.