cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Creating & changing elements

Build, insert, move, clone, replace, and remove DOM elements with createElement, append, DocumentFragment, template, and insertAdjacentHTML.

By the end you can
  • 01
    Build nodes safelyCreate elements, set text or attributes, and attach them only when ready.
  • 02
    Choose the right insertion methodUse append, prepend, before, after, replaceWith, remove, insertBefore, and insertAdjacentHTML with confidence.
  • 03
    Avoid DOM surprisesExplain moving versus cloning, fragments emptying, inert templates, listener cloning, and HTML-injection risk.

Build first, insert second

The previous DOM lessons taught you how to find existing nodes and read their text, HTML, attributes, and properties. This lesson is where the page starts changing shape. You will create brand-new elements, place them around existing ones, move them between parents, clone them, remove them, batch them in fragments, stamp them from templates, and insert trusted HTML strings at precise positions.

The most important mental model is simple: creating a node is not the same as inserting it. A new element can be configured safely while it is still off the page. Only an insertion method makes it part of the document tree that visitors see.

Working definition

To modify the DOM, create or find nodes, change their text or attributes, then insert, move, replace, or remove those nodes in the document tree.

Real-life analogyDOM building is LEGO building

A loose LEGO brick is real before it is snapped onto the board. You can choose its color, add a sticker, and connect smaller bricks to it while it sits in your hand. A DOM element works the same way: create it, prepare it, then attach it when the piece is ready.

In real life: Taking a new brick out of the box
In JavaScript: document.createElement() creates a loose element
In real life: Writing a label on the brick
In JavaScript: textContent, properties, and attributes configure it
In real life: Snapping the brick onto the board
In JavaScript: append, prepend, before, or after inserts it
In real life: Moving a brick already on the board
In JavaScript: Inserting an existing node moves it to the new place
In real life: Photocopying a brick
In JavaScript: cloneNode(true) copies the element and descendants

Where the analogy stops: Real LEGO bricks cannot accidentally keep duplicate ID labels, and photocopies do not have event listeners. In the DOM, deep clones copy attributes but not listeners added with addEventListener, so you must fix IDs and wire behavior yourself.

In the experiments below, every live DOM change happens inside an empty React ref container. React creates the empty box; the DOM APIs fill it. That keeps the demos safe next to React and mirrors how you might progressively enhance a small part of a page.

createElement and safe text

STEP THROUGH

document.createElement("article") returns a new HTMLElement. It has no parent yet, so it is not visible. That off-page moment is useful: you can fill text with textContent, set attributes, append children, and only then insert the finished piece.

Step through building one card
Step 0 of 6Ready
Your turn: follow the blue line

Step through a tiny model of DOM construction. The recording is a pure model so it can render on the server, and the live page verifies the same operations in your browser.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const title = createElement("h3");title.textContent = "DOM field note";card.setAttribute("class", "note-card");card.append(title);console.log(card.outerHTML);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Notice that the replay is a small model, not a paused browser debugger. It records the same create → fill → attach order that the browser runs. The live playgrounds that follow verify the DOM behavior in your real browser.

Ways to put content into a new element
OperationParses HTML?Use it for
element.textContent = valueNoUser text, labels, messages, data from APIs
element.setAttribute(name, value)NoHTML attributes such as aria-label, data-id, or class
element.append(child)Strings become Text nodesAdding child nodes or safe text after existing children
element.innerHTML = htmlYesOnly trusted, intentional markup; not user input
Tiny but important safety rule

If a string is plain text, use textContent. If a string is meant to be markup, treat it as dangerous until you know where it came from and whether it was sanitized.

append, prepend, before, and after

INTERACTIVE

Insertion methods answer two questions: inside or outside? and start or end? append and prepend insert children inside a parent. before and after insert siblings around the target itself.

Insertion positions playground
Methods used in the playgroundPop out in the code editor (opens in a new tab)JavaScript
const list = document.querySelector("#planets");const target = list.querySelector(".target");const badge = document.createElement("li");badge.textContent = "New moon"; list.append(badge);list.prepend(badge);target.before(badge);target.after(badge);target.replaceWith(badge);list.insertAdjacentHTML("beforeend", "<li>HTML comet</li>");
Real DOM outputempty ref container
beforebegin
afterbegintargetbeforeend
afterend
    Try it yourself

    Reset builds the list in an empty ref container, then the buttons run real DOM methods on nodes React is not managing.

    Every button mutates a small DOM tree inside an empty ref. Reusing the same badge proves existing nodes move instead of being copied.

    The modern methods are flexible: append, prepend, before, after, and replaceWith accept any number of nodes and strings. Strings become Text nodes. They are never parsed as HTML. The older appendChild accepts one Node and returns it; the modern append returns undefined. The older insertBefore(newNode, referenceNode) inserts before a reference child, and referenceNode === null means append at the end.

    Moving, cloning, replacing, and removing

    INTERACTIVE

    A node has one parent. If it is already in the document and you insert it somewhere else, the browser moves it. Nothing is copied unless you ask for a clone.

    Moving, not copying
    Move and clone sourcePop out in the code editor (opens in a new tab)JavaScript
    const inbox = document.querySelector("#inbox");const done = document.querySelector("#done");const task = inbox.querySelector("#task-1"); task.addEventListener("click", () => log("original clicked")); // Move: the original leaves inbox and enters done.done.append(task);const shallow = task.cloneNode(false);const deep = task.cloneNode(true);
    Real DOM outputclick list items
      Try it yourself

      Move the original, then add clones and click them. Only the original has the JavaScript-added listener.

      Deep clones copy descendants and attributes, but not event listeners registered with addEventListener. The demo removes duplicate IDs from clones because the browser will not do that for you.
      Move, clone, replace, or remove?
      GoalMethodProfessional note
      Move one existing nodenewParent.append(node)The node leaves its previous parent first.
      Copy just one elementnode.cloneNode(false)Attributes copy; children do not.
      Copy a subtreenode.cloneNode(true)Descendants and attributes copy; addEventListener listeners do not.
      Swap a nodeoldNode.replaceWith(newNode, "text")Accepts nodes and strings, like before and after.
      Detach a nodenode.remove()Returns undefined; parent.removeChild(child) returns the child.

      Deep cloning has two surprises. First, listeners registered with addEventListener do not come along. Second, attributes do copy, including id. If you clone a card with id="profile-card", you now have duplicate IDs unless you fix them.

      DocumentFragment and <template>

      INTERACTIVE

      A DocumentFragment is a lightweight container for nodes. You can assemble many children in it, then append the fragment once. The fragment itself does not become a wrapper element. Its children move into the destination, leaving the fragment empty.

      Real-life analogyA fragment is an assembly tray

      Imagine building a model on a tray, then sliding the finished pieces onto the table. The tray helped you carry the batch, but it is not part of the model.

      In real life: A tray holding 100 little parts
      In JavaScript: A DocumentFragment holding 100 li nodes
      In real life: Slide the parts onto the table
      In JavaScript: Append the fragment to the real list
      In real life: The tray is empty afterward
      In JavaScript: fragment.childNodes.length becomes 0

      Where the analogy stops: A physical tray might stay under the parts. A DocumentFragment does not remain as a visible wrapper in the DOM.

      Real-life analogyA template is a cookie cutter

      A cookie cutter waits until you press it into dough. A <template> also waits without activating its contents. Clone the content, fill in safe text, and insert it.

      In real life: A cutter waits in the kitchen drawer
      In JavaScript: A <template> stays inactive
      In real life: Press out a fresh cookie shape
      In JavaScript: template.content.cloneNode(true)
      In real life: Add icing to each cookie
      In JavaScript: Set text on the cloned nodes

      Where the analogy stops: A cookie cutter is not copied. In the DOM, clone template content for every new instance.

      DocumentFragment and template
      Fragment and template sourcePop out in the code editor (opens in a new tab)JavaScript
      const fragment = document.createDocumentFragment();for (let i = 1; i <= 100; i += 1) {  const item = document.createElement("li");  item.textContent = "Row " + i;  fragment.append(item);}list.append(fragment); // fragment.childNodes.length is now 0 const copy = template.content.cloneNode(true);copy.querySelector("h3").textContent = name;cards.append(copy);
      Real DOM outputnot built
      Try it yourself

      Build the 100-row fragment, then stamp template cards. Fragment count: not built.

      The output is built with real DOM nodes in an empty ref. The template element below is present but inert until cloned.

      insertAdjacentHTML and friends

      INTERACTIVE

      Sometimes you intentionally have a trusted HTML string, such as markup generated by your own code. insertAdjacentHTML(position, html) parses that string at one of four positions: beforebegin, afterbegin, beforeend, or afterend. It does not replace existing siblings or children outside the insertion point.

      insertAdjacentHTML vs innerHTML +=
      Adjacent insertion sourcePop out in the code editor (opens in a new tab)JavaScript
      const panel = document.querySelector(".panel");const input = panel.querySelector("input");input.value = "typed text"; // Safer for trusted markup: does not replace existing nodes.panel.insertAdjacentHTML("beforeend", "<p>Added</p>");// Avoid this pattern for adding: it reparses children.panel.innerHTML += "<p>Added</p>";
      Real DOM outputwatch input state
      Try it yourself

      Type in the input, then try each insertion method.

      The HTML strings are predefined and trusted for the demo. Never parse learner-typed or untrusted strings directly into the lesson page.
      Security warning: HTML strings can execute

      Do not pass untrusted strings to innerHTML, outerHTML, or insertAdjacentHTML. A string can contain event handler attributes, dangerous URLs, or script-like behavior. Prefer textContent for data, or sanitize on a trusted path before parsing.

      There are two related methods with the same four positions: insertAdjacentElement(position, element) inserts an element node, and insertAdjacentText(position, text) inserts text without parsing HTML. To clear or replace all children in one readable operation, use replaceChildren().

      Accepts a string? Parses HTML?
      • append("<b>hi</b>")
      • textContent = "<b>hi</b>"
      • appendChild(node)
      • target.before("label", icon)
      • insertAdjacentHTML("beforeend", html)
      • innerHTML += html
      • old.replaceWith(newNode, "saved")
      • node.remove()
      Try it yourself
      0 of 8 correct

      Sort each method by what it accepts or does. Watch especially for strings: most insertion methods treat them as text, not markup.

      Choose a category for every card. You can change an answer at any time; Reset clears them all.
      Sorter reference callsPop out in the code editor (opens in a new tab)JavaScript
      container.append("<b>hi</b>");
      container.appendChild(document.createElement("b"));
      target.before("label", icon);
      list.insertBefore(item, null);
      panel.insertAdjacentHTML("beforeend", "<b>hi</b>");
      box.textContent = "<b>hi</b>";
      oldCard.replaceWith(newCard, " saved");

      Where you will use this

      You will use these APIs any time a page needs to render data without a full refresh: search results, todo lists, validation messages, notifications, empty states, and small widgets on server-rendered pages. Frameworks like React usually generate and update DOM nodes for you, but knowing the underlying operations makes debugging much easier.

      Render a list from dataPop out in the code editor (opens in a new tab)JavaScript
      function renderTodos(todos) {
        const list = document.createElement("ul");
      
        for (const todo of todos) {
          const item = document.createElement("li");
          item.textContent = todo.title;
          if (todo.done) item.setAttribute("data-done", "true");
          list.append(item);
        }
      
        app.replaceChildren(list);
      }

      This function creates a fresh list, fills each li with safe text, then uses replaceChildren to swap the app’s children in one step. If the list had old loading text, it disappears. If no arguments are passed, app.replaceChildren() simply clears the container.

      Newer browser note

      Element.moveBefore() is a newer state-preserving move API in some browsers. It is not everywhere, so feature-detect it before use. The methods in this lesson are the standard baseline you will see most often.

      Common misconceptions

      • “createElement shows it immediately.” No. Creation makes a loose node. Insertion makes it visible.
      • “append copies existing nodes.” No. Existing nodes move because a node has one parent.
      • “append parses HTML strings.” No. Strings become Text nodes. Use HTML parsing APIs only for trusted markup.
      • “cloneNode(true) copies everything.” It copies attributes and descendants, not listeners added with addEventListener or most JavaScript state.
      • “A fragment remains as a wrapper.” No. Inserting a fragment moves its children and leaves it empty.
      • “innerHTML += is just append for HTML.” It reparses and replaces children, which can disturb input state and listeners.

      Practice exercises

      5 EXERCISES
      Exercise 1 · Warm-upCreate, fill, insert

      Create an li, give it safe text, and insert it into a list.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const item = document.createElement("li");
      // Add the text "Read DOM lesson" safely.
      // Insert it at the end of list.
        Exercise 2 · Warm-upPredict append with a string

        Predict the logged type.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const fakeElement = {
          append(...items) {
            console.log(typeof items[0]);
          },
        };
        fakeElement.append("<b>hi</b>");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 3 · PracticeTrack a fragment

          Use this pure model to predict the count after a fragment insertion.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const fragment = { childNodes: ["a", "b", "c"] };
          const list = [];
          list.push(...fragment.childNodes);
          fragment.childNodes.length = 0;
          console.log(list.length + ":" + fragment.childNodes.length);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 4 · PracticeFind the move

            Predict the output of this moving model.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const inbox = ["A", "B"];
            const done = [];
            const item = inbox.splice(inbox.indexOf("B"), 1)[0];
            done.push(item);
            console.log(inbox.join("") + "|" + done.join(""));

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 5 · ChallengeRender safely and clear old content

              Write the body of renderMessage. Which method clears all old children?

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              function renderMessage(container, message) {
                const p = document.createElement("p");
                // Fill p safely, then replace all old children.
              }

              Answer, then press Check. Spacing and letter case don’t matter.

                Check your understanding

                8 QUESTIONS
                Creating and changing elements quiz · 8 questionsScore: first tries count
                1. Question 1 of 8What does document.createElement("li") do immediately?

                  Choose an answer to see the explanation.

                2. Question 2 of 8What happens when you append a node that is already in another list?

                  Choose an answer to see the explanation.

                3. Question 3 of 8Which call parses the string as HTML?

                  Choose an answer to see the explanation.

                4. Question 4 of 8What does this print in the model from the exercises?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  const inbox = ["A", "B"];
                  const done = [];
                  const item = inbox.splice(inbox.indexOf("B"), 1)[0];
                  done.push(item);
                  console.log(inbox.join("") + "|" + done.join(""));

                  Choose an answer to see the explanation.

                5. Question 5 of 8Which statement about cloneNode(true) is correct?

                  Choose an answer to see the explanation.

                6. Question 6 of 8After you append a DocumentFragment, what is true about the fragment?

                  Choose an answer to see the explanation.

                7. Question 7 of 8Which insertAdjacentHTML position inserts as the first child of the target element?

                  Choose an answer to see the explanation.

                8. Question 8 of 8Which method replaces all children of an element when you pass new children, or clears it with no arguments?

                  Choose an answer to see the explanation.

                Key takeaways

                • createElement creates a loose element; insertion methods attach it to the document.
                • append, prepend, before, after, and replaceWith accept nodes and strings; strings become Text nodes.
                • Inserting an existing node moves it. Use cloneNode for copies, then fix duplicate IDs and reattach JavaScript listeners.
                • DocumentFragment batches children and empties when inserted. template.content is an inert fragment you clone.
                • insertAdjacentHTML parses trusted HTML at a precise position. Use textContent for untrusted text.

                Final definition: DOM modification is the art of creating or finding nodes, filling them safely, and deliberately moving, copying, replacing, or removing them in the document tree.

                Up next: Styles & classes.

                CompleteFrontend Clear concepts. Working examples.