ESC

Type to search the knowledge base.

Creating and Updating DOM Nodes

createElement, textContent vs innerHTML, insert APIs, and batching updates — safe DOM writes without accidental XSS.

beginner3 min read
  • javascript
  • dom
  • createElement
  • innerHTML

Direct DOM work still shows up in widgets, design-system primitives, and interviews. The skill is less “know every method” and more create safely, insert once, update without thrashing.

Create and attach

const li = document.createElement('li');
li.className = 'todo-item';
li.textContent = 'Ship the PR'; // safe for untrusted text

const list = document.querySelector('#todos');
list.append(li); // modern; also appendChild
const frag = document.createDocumentFragment();
for (const t of todos) {
  const li = document.createElement('li');
  li.textContent = t.title;
  frag.append(li);
}
list.append(frag); // one reflow for many children

See DocumentFragment for bulk inserts.

textContent vs innerHTML vs innerText

API Use
textContent Plain text; no HTML parse; safe for user strings
innerHTML Parse HTML; XSS if string is untrusted
innerText Layout-aware text; slower; triggers reflow
el.textContent = userName; // correct default

// only with trusted/sanitized HTML
el.innerHTML = trustedMarkup;

// build structure without HTML strings
const a = document.createElement('a');
a.href = url; // still validate urls (javascript: etc.)
a.textContent = label;
el.replaceChildren(a);

Insert positions

parent.append(child); // last
parent.prepend(child); // first
parent.replaceChildren(...nodes); // clear + set
old.replaceWith(newNode);
ref.before(node);
ref.after(node);

// legacy
parent.insertBefore(node, ref);
// move existing node — append moves, does not clone
list.append(list.firstElementChild);

Attributes and properties

img.setAttribute('alt', 'Chart');
img.alt = 'Chart'; // property — preferred for standard props

input.value = 'hello'; // property reflects live state
checkbox.checked = true;

// data-*
el.dataset.id = '42';

Boolean attributes: prefer properties (disabled, checked) over fiddling with attribute strings.

Update without nuking event listeners

// hard reset — destroys listeners on descendants
panel.innerHTML = '';

// better: remove nodes explicitly or replaceChildren
panel.replaceChildren();

// update text only
titleEl.textContent = nextTitle;

If you re-create a whole subtree every keystroke, you will drop focus and listeners. Patch the changed leaves instead — or use a framework.

XSS checklist (write it on the board)

  1. Untrusted data → textContent or escape.
  2. URLs → allowlist protocol (https:, mailto:).
  3. Never innerHTML = request.body.
  4. Sanitize if you must render HTML (server-side ideally).
function setLink(a, href, label) {
  const url = new URL(href, window.location.origin);
  if (!['http:', 'https:'].includes(url.protocol)) {
    throw new Error('blocked protocol');
  }
  a.href = url.href;
  a.textContent = label;
}

Interview answer

“I create nodes with createElement, set text via textContent for safety, and batch inserts with a DocumentFragment. I avoid innerHTML for untrusted data. Updates should change the minimum nodes so focus and listeners survive. append/prepend/replaceWith are the modern insert APIs.”

Document positions and ranges

// insert HTML string at a position without wiping the parent (trusted HTML only)
title.insertAdjacentHTML('afterend', '<p class="sub">Trusted</p>');
// positions: beforebegin, afterbegin, beforeend, afterend

// clone
const node = template.content.firstElementChild.cloneNode(true);
node.querySelector('.label').textContent = label; // fill after clone
list.append(node);

cloneNode(true) deep-clones subtree but not event listeners added via addEventListener (inline onclick attributes are copied). Prefer re-binding or using delegation on a parent.

For lists that update often, identity-stable nodes (reuse by key) beat wipe-and-rebuild — same idea as React reconciliation, even in vanilla code.

Further reading

Related guides