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);
  • append and prepend accept strings as well as nodes and take several arguments; appendChild takes one node and returns it.
  • A DocumentFragment is 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
TaskModern callOlder equivalent
Insert at the endparent.append(node)appendChild(node)
Insert at the startparent.prepend(node)insertBefore(node, firstChild)
Replace a nodeold.replaceWith(node)replaceChild(node, old)
Remove a nodenode.remove()parent.removeChild(node)
Empty a nodeparent.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.
  • children is live; spread it or copy it with Array.from before removing anything inside the loop.
  • Go upwards with closest(), downwards with querySelectorAll(), and sideways with the sibling properties.
  • childNodes.length counts 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.

Reading and updating content safely Events

Last refreshed 2026-09-18.