Modifying the document

Reading the DOM tells you what’s on the page. Changing it is how a page comes alive: menus open, items get added to a cart, notifications slide in and fade out. All of that is DOM modification.

This lesson covers how to build new nodes from scratch, drop them into exactly the spot you want, move and clone them, and take them back out again.

Example: show a message

Start with something concrete. Instead of a jarring alert, suppose you want to show a styled banner on the page — a blue “saved” toast.

Here’s the target, written as plain HTML:

<style>
.toast {
  padding: 15px;
  border: 1px solid #bfdbfe;
  border-radius: 4px;
  color: #1e40af;
  background-color: #dbeafe;
}
</style>

<div class="toast">
  <strong>Saved!</strong> Your draft is safely stored.
</div>

That’s static markup. The interesting question is how to produce that same <div> from JavaScript, at any moment you choose, assuming the .toast styles already live in the page’s CSS.

Creating an element

Before anything shows up on screen, you build the node in memory. There are two creation methods, one per node type.

document.createElement(tag) builds a new element node from a tag name:

let div = document.createElement('div');

document.createTextNode(text) builds a new text node holding the given text:

let textNode = document.createTextNode('Here I am');

Element nodes are what you reach for almost every time, since they’re the tags that carry structure and styling. Text nodes come up far less often because there are shorter ways to put text on the page, which you’ll see below.

Creating the message

Building the toast div is three small steps: make the element, give it a class, fill it with content.

// 1. Create <div> element
let div = document.createElement('div');

// 2. Set its class to "toast"
div.className = "toast";

// 3. Fill it with the content
div.innerHTML = "<strong>Saved!</strong> Your draft is safely stored.";

The element now exists, fully formed. There’s a catch that trips up every beginner at least once: it isn’t on the page. The variable div points to a node that lives off to the side, disconnected from the document tree. Nothing renders until you attach it.

document tree
<html>
<body>
no toast here yet
detached (in memory)
div
<div class=“toast”>
A freshly created element is 'detached' — it exists in memory but is not part of the rendered document tree.

Insertion methods

To make the div visible, put it somewhere in the document. The obvious target is the page body, reachable as document.body.

The method for that is append:

document.body.append(div);

Here’s the whole thing wired together:

<style>
.toast {
  padding: 15px;
  border: 1px solid #bfdbfe;
  border-radius: 4px;
  color: #1e40af;
  background-color: #dbeafe;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "toast";
  div.innerHTML = "<strong>Saved!</strong> Your draft is safely stored.";

  document.body.append(div);
</script>

append isn’t special to document.body. Call it on any element to drop something inside it: someDiv.append(anotherElement) puts anotherElement at the end of someDiv.

There’s a small family of these methods, and each one targets a different spot relative to the node you call it on:

  • node.append(...nodes or strings) — insert at the end, inside node.
  • node.prepend(...nodes or strings) — insert at the beginning, inside node.
  • node.before(...nodes or strings) — insert before node (as a sibling).
  • node.after(...nodes or strings) — insert after node (as a sibling).
  • node.replaceWith(...nodes or strings) — swap node out for whatever you pass.

Every one of them takes any number of arguments, and each argument is either a DOM node or a string. Strings turn into text nodes on the way in.

Seeing all four at once makes the geometry click. This example seeds text around a list and slots list items at both ends:

<ol id="ol">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  ol.before('before'); // insert string "before" before <ol>
  ol.after('after'); // insert string "after" after <ol>

  let liFirst = document.createElement('li');
  liFirst.innerHTML = 'prepend';
  ol.prepend(liFirst); // insert liFirst at the beginning of <ol>

  let liLast = document.createElement('li');
  liLast.innerHTML = 'append';
  ol.append(liLast); // insert liLast at the end of <ol>
</script>

The two before/after calls land outside the list; the two prepend/append calls land inside it. Here’s the map:

before ← sibling, just outside the top
<ol>
prepend ← first child
0
1
2
append ← last child
</ol>
after ← sibling, just outside the bottom
before / after attach as siblings outside the element; prepend / append attach as children inside it.

So the rendered result is:

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after

Reading about the four slots is one thing; watching a node drop into each is another. Fire the buttons and see where every new item lands — prepend/append go inside the list, before/after go outside it as siblings:

interactiveWhere does each method insert?

Because each method accepts a list, you can mix nodes and text in a single call. This one inserts a string and a freshly built <hr> before a div:

<div id="div"></div>
<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

Notice what happens to that '<p>Hello</p>' string. It goes in as text, not as markup. The angle brackets get escaped, so the browser shows the literal characters instead of building a paragraph:

&lt;p&gt;Hello&lt;/p&gt;
<hr>
<div id="div"></div>

That’s the same safe behavior you get from elem.textContent. It’s a feature, not a bug — inserting user-supplied strings this way can’t smuggle in <script> tags or other markup.

insertAdjacentHTML/Text/Element

When you do want a string parsed as real HTML — tags, nesting, and all — reach for elem.insertAdjacentHTML(where, html).

The first argument is a keyword naming the insertion point relative to elem. It’s one of four:

  • "beforebegin" — just before elem, outside it.
  • "afterbegin" — inside elem, at the very start.
  • "beforeend" — inside elem, at the very end.
  • "afterend" — just after elem, outside it.

The second argument is the HTML string, and this time it’s parsed as HTML.

<div id="div"></div>
<script>
  div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
  div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

That produces real paragraph elements wrapped around the div:

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>

The four keywords line up exactly with the four positions from before/prepend/append/after — same spots, different input. A memory trick: the word starts with before/after (the outside positions) or ends with begin/end (the inside positions).

“beforebegin” ← outside, before elem
<div id=“div”>
“afterbegin” ← inside, at the start
…existing content…
“beforeend” ← inside, at the end
</div>
“afterend” ← outside, after elem
The four insertAdjacent keywords map to the same slots as before / prepend / append / after.

The method has two siblings that share its syntax:

  • elem.insertAdjacentText(where, text) — inserts a string as text, with escaping.
  • elem.insertAdjacentElement(where, elem) — inserts an element node.

Those two exist mostly for symmetry. In day-to-day code you rarely see them, because append/prepend/before/after already cover text and elements with less typing and the ability to pass several things at once. insertAdjacentHTML earns its keep because nothing else in this group parses HTML.

Here’s the toast message again, this time built with a single insertAdjacentHTML call. "afterbegin" on document.body drops it at the top of the page:

<style>
.toast {
  padding: 15px;
  border: 1px solid #bfdbfe;
  border-radius: 4px;
  color: #1e40af;
  background-color: #dbeafe;
}
</style>

<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="toast">
    <strong>Saved!</strong> Your draft is safely stored.
  </div>`);
</script>

The difference is easiest to feel by inserting the same string both ways. Edit the text below (leave the tags in), then insert it as text with append and as markup with insertAdjacentHTML — one shows the literal angle brackets, the other builds real elements:

interactiveString as text vs. string as HTML

Node removal

Taking a node out is a one-liner: node.remove().

Let’s make the toast vanish after a second, using setTimeout:

<style>
.toast {
  padding: 15px;
  border: 1px solid #bfdbfe;
  border-radius: 4px;
  color: #1e40af;
  background-color: #dbeafe;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "toast";
  div.innerHTML = "<strong>Saved!</strong> Your draft is safely stored.";

  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
</script>

Removing detaches the node from the tree, but the variable still points at it. So you can hold onto a removed node and re-insert it later if you want.

That leads to a genuinely useful trick. To move an existing node, you don’t remove it first. The insertion methods relocate a node automatically — if the node is already in the document, the browser pulls it from its old spot and drops it in the new one.

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // no need to call remove
  second.after(first); // take #second and after it insert #first
</script>

After this runs the order flips to “Second, First”. A single node can’t exist in two places at once, so inserting an already-attached node is always a move.

Here it is in motion. Each button re-inserts an existing tile — notice nothing is ever duplicated, because a single node can only live in one place at a time:

interactiveInserting an existing node moves it

Cloning nodes: cloneNode

Say you want to show a second, similar message. You could wrap the creation code in a function and call it twice. Another route: clone the node you already have and tweak the copy.

For a large, richly-structured element that can be quicker and easier than rebuilding it piece by piece.

elem.cloneNode(true) makes a deep clone — the element plus every attribute and every descendant. Pass false and you get a shallow clone: just the element itself, no children.

<style>
.toast {
  padding: 15px;
  border: 1px solid #bfdbfe;
  border-radius: 4px;
  color: #1e40af;
  background-color: #dbeafe;
}
</style>

<div class="toast" id="div">
  <strong>Saved!</strong> Your draft is safely stored.
</div>

<script>
  let div2 = div.cloneNode(true); // clone the message
  div2.querySelector('strong').innerHTML = 'Synced!'; // change the clone
  div.after(div2); // show the clone after the existing div
</script>

The clone starts life detached, exactly like a createElement result, which is why the last line has to insert it. Editing div2 leaves the original div untouched — they’re separate node trees.

original (div)
<div class=“toast”>
<strong>Saved!</strong>
</div>
clone (div2), edited
<div class=“toast”>
<strong>Synced!</strong>
</div>
cloneNode(true) copies the whole subtree; the copy is independent of the original.

Clone the card as many times as you like — each copy gets its own edited title, while the original at the top never changes:

interactivecloneNode(true) makes an independent copy

DocumentFragment

DocumentFragment is a lightweight, invisible container node. Its whole purpose is to carry a group of nodes around. Append nodes into a fragment, then insert the fragment somewhere, and only its children land — the fragment wrapper itself dissolves.

Here getListContent builds a fragment full of <li> items and returns it, and the caller drops those items straight into a <ul>:

<ul id="ul"></ul>

<script>
function getListContent() {
  let fragment = new DocumentFragment();

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }

  return fragment;
}

ul.append(getListContent()); // (*)
</script>

At line (*) you append a DocumentFragment, yet it doesn’t show up in the tree. Only its contents do:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
fragment
<li>1
<li>2
<li>3
append →
<ul>
<li>1
<li>2
<li>3
A DocumentFragment holds nodes, but on insertion only its children pass through — the wrapper never appears in the DOM.

In practice you rarely create one by hand. Why funnel nodes through a special wrapper when you can collect them in a plain array and spread them into append? Same result, more familiar tools:

<ul id="ul"></ul>

<script>
function getListContent() {
  let result = [];

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }

  return result;
}

ul.append(...getListContent()); // append + "..." operator = friends!
</script>

DocumentFragment still matters as a concept, though, because features are built on top of it — the <template> element, which you’ll meet later, uses fragments under the hood.

Old-school insert/remove methods

Long before append and its friends existed, the DOM shipped with a clunkier set of methods. They still work — browsers keep them for backward compatibility — but modern code has no reason to reach for them, since the newer methods do more with less. You’ll want to recognize them, because old scripts are full of them.

parentElem.appendChild(node) — adds node as the last child of parentElem.

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.appendChild(newLi);
</script>

parentElem.insertBefore(node, nextSibling) — inserts node right before nextSibling inside parentElem.

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.insertBefore(newLi, list.children[1]);
</script>

Passing list.firstChild as the reference makes it the first element:

list.insertBefore(newLi, list.firstChild);

parentElem.replaceChild(node, oldChild) — swaps oldChild out for node among the children of parentElem.

parentElem.removeChild(node) — removes node from parentElem, assuming node really is its child.

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let li = list.firstElementChild;
  list.removeChild(li);
</script>

Notice the awkward part: these are all methods on the parent, so you constantly need a reference to the parent node. The modern node.remove() or node.replaceWith(...) act on the node itself — no parent lookup. That’s the main reason they’ve taken over.

Every one of these old methods returns the node it inserted or removed. parentElem.appendChild(node) hands back node. Usually nobody reads the return value; you just call the method for its effect.

A word about “document.write”

One more relic deserves a mention: document.write.

<p>Somewhere in the page...</p>
<script>
  document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

document.write(html) dumps its string into the page “right here, right now”, at the spot where the script runs. The string can be built dynamically, so in principle you could generate a whole page and write it out.

It dates from an era before the DOM existed as a standard — truly early web. It survives because old scripts still call it.

Modern code avoids it, and the reason is a sharp limitation:

document.write only does something useful while the page is still loading.

Call it after the page has finished loading and it wipes the existing document clean before writing:

<p>After one second the contents of this page will be replaced...</p>
<script>
  // document.write after 1 second
  // that's after the page loaded, so it erases the existing content
  setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

So for anything happening after load, it’s effectively unusable, unlike every DOM method above. That’s the downside.

There’s an upside too. While the browser is still parsing incoming HTML, a document.write call feeds text into that stream as though it had been in the original markup. No DOM nodes get created and rearranged — the text goes straight into the parse buffer, which is very fast.

So if you need to pour a large amount of markup into the page during loading, and raw speed is the priority, it can win. Those conditions rarely line up in practice, and most sightings of document.write are just old code, not a deliberate performance choice.

Summary

Methods to create new nodes:

  • document.createElement(tag) — creates an element with the given tag.
  • document.createTextNode(value) — creates a text node (rarely needed).
  • elem.cloneNode(deep) — clones the element; deep == true copies all descendants too.

Insertion and removal (call these on the node itself):

  • node.append(...nodes or strings) — insert inside, at the end.
  • node.prepend(...nodes or strings) — insert inside, at the beginning.
  • node.before(...nodes or strings) — insert right before node.
  • node.after(...nodes or strings) — insert right after node.
  • node.replaceWith(...nodes or strings) — replace node.
  • node.remove() — remove node.

Strings passed to these go in as text, escaped and safe. Inserting an already-attached node moves it.

The “old school” equivalents, all called on the parent and all returning node:

  • parent.appendChild(node)
  • parent.insertBefore(node, nextSibling)
  • parent.removeChild(node)
  • parent.replaceChild(newElem, node)

For inserting a parsed HTML string, elem.insertAdjacentHTML(where, html) places it by keyword:

  • "beforebegin" — right before elem.
  • "afterbegin" — inside elem, at the start.
  • "beforeend" — inside elem, at the end.
  • "afterend" — right after elem.

Its siblings elem.insertAdjacentText and elem.insertAdjacentElement share the syntax but see little use.

And to write markup into the page before it finishes loading, document.write(html) — which erases the document if called afterward, and mostly turns up in old scripts.