Creating and Updating DOM Nodes
createElement, textContent vs innerHTML, insert APIs, and batching updates — safe DOM writes without accidental XSS.
- 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)
- Untrusted data →
textContentor escape. - URLs → allowlist protocol (
https:,mailto:). - Never
innerHTML = request.body. - 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.”
Related
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
- classList and datasetToggle CSS classes with classList and read data-* attributes via dataset — tokens, naming, and DOM performance notes.
- client, offset, and scroll DimensionsclientWidth vs offsetWidth vs scrollWidth — borders, scrollbars, and which box measurement to use for layout math.
- contentEditable PitfallsWhy contentEditable is hard: browser HTML mess, carets, sanitization, undo, and when to pick a real editor framework.
- Custom EventsCustomEvent, detail payloads, bubbles and composed — decoupling components without a global event bus mess.
- DocumentFragmentBuild subtrees off-DOM with DocumentFragment — one insert, fewer reflows, and how it differs from a wrapper div.