What you'll be able to do

By the end of this page you should be able to create elements in code, append them to the page, render an array as a list, and choose textContent over innerHTML when content is not fully trusted.

Previous post: DOM traversal.

Who this is for

  • People who stuff HTML strings into the page and wonder why things break
  • Beginners who finished select/change and need to add new nodes
  • Anyone building a todo list from an array

You can skip this if you already build lists with createElement daily. Come back when innerHTML += causes duplicate listeners or XSS scares you.

createElement and append

const note = document.createElement("p");
note.textContent = "Saved.";
document.body.append(note);

Scenario A — one new paragraph after save.

Scenario B — append to a container list.append(li) keeps the page organized.

append accepts multiple nodes; appendChild is the older single-child API — both appear in the wild.

Build a list from an array

const tasks = ["Write script", "Test page"];
const ul = document.querySelector("#tasks");
ul.replaceChildren(); // clear old rows
for (const title of tasks) {
  const li = document.createElement("li");
  li.textContent = title;
  ul.append(li);
}

Scenario A — fresh render after fetch (fetch comes later; the DOM pattern is the same).

Scenario B — empty list. Loop runs zero times — show an empty-state message separately if you need one.

innerHTML: fast but risky with user data

// OK for static template you control:
container.innerHTML = "<p>Loading…</p>";

// Risky if title comes from a user:
li.innerHTML = "<strong>" + userTitle + "</strong>"; // XSS if title has markup

Scenario A — your own fixed markup. innerHTML can be fine.

Scenario B — names, comments, search terms. Set textContent on elements you create, or sanitize with care later.

DocumentFragment (optional speed habit)

const frag = document.createDocumentFragment();
for (const title of tasks) {
  const li = document.createElement("li");
  li.textContent = title;
  frag.append(li);
}
ul.append(frag);

Scenario A — many rows. One append to the live DOM reduces reflow flicker.

Scenario B — three items. Plain loop append is fine.

A tiny practice file

Save as create-list.html.

<!DOCTYPE html>
<html>
  <body>
    <button id="add">Add item</button>
    <ul id="list"></ul>
    <script>
      let n = 0;
      document.getElementById("add").addEventListener("click", function () {
        n += 1;
        const li = document.createElement("li");
        li.textContent = "Item " + n;
        document.getElementById("list").append(li);
      });
    </script>
  </body>
</html>

Mistakes I see a lot

1. innerHTML += in a loop — re-parses whole block; drops event listeners on old nodes.

2. Forgetting to clear the list before re-render — duplicates pile up.

3. Creating elements but never appending — they exist only in memory.

4. Using innerHTML for user text.

5. Hardcoding id on every dynamic row — classes or data-* scale better (post #49).

What to try before the next post

  1. Render three strings into a ul.
  2. Add one row per button click with createElement.
  3. Compare textContent vs innerHTML for a string with <b> in it.
  4. Build create-list.html.

Next in this series: forms and user input — reading values, empty strings, and light validation.

Try this next outside the series

Browser JavaScript stops feeling isolated once you connect it to forms and component-oriented UI work.