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
- Render three strings into a
ul. - Add one row per button click with
createElement. - Compare
textContentvsinnerHTMLfor a string with<b>in it. - 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.
- React forms without pain — see modern form handling after you understand the DOM version first
- HTML to JSX Converter — move tiny HTML snippets into React-style markup when you're ready