ποΈ Creating and Appending Elements
Selecting elements that already exist is only half of DOM work. The other half is building new elements in JavaScript and slotting them into the page β the technique behind every list that grows, every card that renders from data, and every "load more" button you've ever clicked.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Create fresh elements and text nodes with
document.createElement()andcreateTextNode() - Configure an element's content, attributes, classes, and styles before it enters the page
- Insert nodes precisely using
appendChild,append,prepend,insertBefore, andinsertAdjacentElement - Render lists from data and batch inserts with a
DocumentFragmentfor performance - Choose safely between
textContentandinnerHTMLto avoid XSS
Estimated Time: 30β40 minutes β’ Difficulty: BeginnerβIntermediate
Hands-on: Build a small "add task" list that creates, appends, and removes DOM nodes.
In This Lesson
Building the DOM Dynamically
In the last lesson you learned to find elements that the browser already parsed from your HTML. But real applications rarely ship all their content in the initial markup β a chat app adds messages as they arrive, a store renders products fetched from an API, a to-do list grows as you type. All of that is dynamic DOM creation: making brand-new elements in JavaScript and inserting them so the browser paints them to the screen.
The workflow is always the same three moves. You create a node, you configure it (text, attributes, classes), and then you insert it into a parent that is already on the page.
π‘ Analogy β assembling flat-pack furniture. Creating an element is opening the box and taking out the parts. Configuring it is following the instructions to attach the panels and knobs. Appending it to the DOM is carrying the finished chair into the room. You wouldn't try to sit on the chair mid-assembly β and a half-built element isn't visible or clickable until it's placed in the page.
Creating Elements & Text Nodes
The workhorse is document.createElement(tagName). You pass the tag name as a string and get back a new, empty element that lives only in memory:
const paragraph = document.createElement('p');
const button = document.createElement('button');
const listItem = document.createElement('li');
const card = document.createElement('div');
β οΈ Nothing is visible yet
A freshly created element exists in memory but is not part of the page. It has no parent, occupies no space, and cannot be seen or clicked until you explicitly insert it. Forgetting this final step is the single most common "why isn't my element showing up?" bug.
You can also create standalone text and comment nodes, though you'll usually set text with a property instead:
const textNode = document.createTextNode('Hello, world!');
const commentNode = document.createComment('rendered from JS');
Configuring an Element
Once you have an element, set its content, attributes, classes, and styles. Do this before inserting it β configuring an off-page node is faster because the browser doesn't have to re-render after each change.
Text vs. HTML content
const p = document.createElement('p');
// Safest: assigns plain text. Any < or > is shown literally.
p.textContent = 'This is a new paragraph.';
// Parses a string as HTML. Powerful, but risky with untrusted data.
const box = document.createElement('div');
box.innerHTML = '<strong>Bold</strong> and <em>italic</em> text.';
π textContent vs. innerHTML
textContent sets text only β HTML characters are escaped automatically, so it is safe for user input.
innerHTML parses the string as markup and builds real elements. Convenient, but if the string contains user data it can inject a script β a cross-site scripting (XSS) attack. Reach for textContent by default.
Attributes
const img = document.createElement('img');
// Standard attributes have direct properties:
img.src = '/images/example.jpg';
img.alt = 'A scenic landscape';
img.loading = 'lazy';
// setAttribute works for anything, including custom data-* attributes:
img.setAttribute('data-category', 'nature');
img.setAttribute('width', '300');
Classes
const btn = document.createElement('button');
btn.classList.add('btn', 'btn-primary'); // add one or many
btn.classList.toggle('active'); // add if missing, remove if present
if (btn.classList.contains('btn')) { /* ... */ }
btn.classList.remove('active');
Inline styles
const card = document.createElement('div');
card.style.backgroundColor = '#f5f5f5'; // note camelCase: background-color β backgroundColor
card.style.padding = '20px';
card.style.borderRadius = '8px';
π‘ Prefer classes over inline styles
For anything more than a one-off calculated value, add a CSS class instead of setting many style.* properties. It keeps presentation in your stylesheet and behavior in your JavaScript β easier to maintain and to theme.
Inserting Into the DOM
This is the step that makes an element real. There are several insertion methods, each giving you a different amount of control over where the node lands.
appendChild() and append()
const container = document.getElementById('content');
const p = document.createElement('p');
p.textContent = 'Added as the last child.';
container.appendChild(p); // classic: one Node only, returns it
// append() is newer and friendlier: multiple nodes AND plain strings, returns nothing
const h2 = document.createElement('h2');
h2.textContent = 'Product Details';
container.append(h2, 'Some trailing text', p);
prepend() β add to the beginning
const banner = document.createElement('div');
banner.textContent = 'Important announcement';
container.prepend(banner); // becomes the first child
insertBefore() β position relative to a sibling
const list = document.getElementById('my-list');
const newItem = document.createElement('li');
newItem.textContent = 'Inserted item';
const referenceItem = list.children[2]; // the current third item
list.insertBefore(newItem, referenceItem); // parent.insertBefore(newNode, referenceNode)
insertAdjacentElement() β the precise one
This method places a node at one of four positions relative to a reference element, which is often exactly the control you want:
const notice = document.createElement('div');
notice.textContent = 'Please read carefully.';
const title = document.getElementById('form-title');
title.insertAdjacentElement('beforebegin', notice); // before the element itself
// 'afterbegin' β inside, before its first child
// 'beforeend' β inside, after its last child
// 'afterend' β after the element itself
insertAdjacentElement (and its siblings insertAdjacentHTML / insertAdjacentText).Rendering From Data
The most common real-world job is turning an array of data into DOM. You loop over the data, build a small subtree for each item, and append it. Here we render a list of products fetched from (a stand-in for) an API:
const products = [
{ id: 1, name: 'Laptop', price: 999.99, inStock: true },
{ id: 2, name: 'Smartphone', price: 699.99, inStock: false },
{ id: 3, name: 'Headphones', price: 149.99, inStock: true },
];
const list = document.getElementById('product-list');
for (const product of products) {
const card = document.createElement('div');
card.className = 'product-card';
card.dataset.productId = product.id; // sets data-product-id
const name = document.createElement('h3');
name.textContent = product.name; // textContent β safe for any name
const price = document.createElement('div');
price.className = 'price';
price.textContent = `$${product.price.toFixed(2)}`;
const stock = document.createElement('span');
stock.className = product.inStock ? 'in-stock' : 'out-of-stock';
stock.textContent = product.inStock ? 'In stock' : 'Out of stock';
card.append(name, price, stock); // one call, multiple children
list.append(card);
}
β
Why textContent here?
Product names could contain characters like & or <. Using textContent means the browser shows them literally and never treats them as markup β so a product mischievously named <img onerror=β¦> can't run code. This safety-by-default is exactly why we build nodes instead of concatenating HTML strings.
Batching With DocumentFragment
Every time you append to a node that's already on the page, the browser may recalculate layout ("reflow") and repaint. Appending 500 list items one at a time can trigger 500 reflows. A DocumentFragment is a lightweight, off-screen container: you append everything to it (no reflows, because it's not in the document), then insert the fragment once.
const fragment = document.createDocumentFragment();
for (let i = 1; i <= 500; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // no reflow β fragment is off-screen
}
// A single DOM operation inserts all 500 items at once.
document.getElementById('my-list').appendChild(fragment);
π‘ The fragment "empties" on insert
When you append a fragment, its children move into the target β the fragment itself is left empty, not nested inside. Think of it as a disposable tray you use to carry many items in one trip.
Cloning & <template>
When many items share the same structure, cloning can be cleaner than building each one from scratch. cloneNode(deep) copies a node; pass true to also copy all descendants:
const original = document.querySelector('.item-template');
const shallow = original.cloneNode(false); // just the element
const deep = original.cloneNode(true); // element + all children
deep.querySelector('.item-title').textContent = 'New title';
document.getElementById('items').append(deep);
The modern, purpose-built tool is the HTML <template> element. Its contents are parsed but inert β never rendered β until you clone them. This keeps your markup declarative and readable:
<template id="card-tpl">
<article class="card">
<h3 class="card-title"></h3>
<p class="card-body"></p>
</article>
</template>
const tpl = document.getElementById('card-tpl');
function renderCard({ title, body }) {
const clone = tpl.content.cloneNode(true); // a fragment
clone.querySelector('.card-title').textContent = title;
clone.querySelector('.card-body').textContent = body;
document.getElementById('feed').append(clone);
}
renderCard({ title: 'Hello', body: 'Rendered from a template.' });
β οΈ Cloning does not copy event listeners
cloneNode copies attributes and inline handlers written in HTML, but not listeners you added with addEventListener. Re-attach behavior after cloning, or better still, use event delegation on a shared parent (see Best Practices).
Hands-on Exercise
ποΈ Build an "Add Task" List
Objective: Practice create β configure β insert, plus removal, in a tiny working app.
Starter markup
<form id="task-form">
<input id="task-input" placeholder="New task" required>
<button>Add</button>
</form>
<ul id="task-list"></ul>
Your goal
- On form submit, create an
<li>whose text is the input's value (usetextContent). - Give each item a "Delete" button that removes its own
<li>. - Clear and refocus the input after adding.
- Stretch: ignore empty/whitespace-only input, and clicking an item's text toggles a
doneclass.
π‘ Hint
Call event.preventDefault() so the form doesn't reload the page. Build the whole <li> (text + button) before appending. For the delete button, li.remove() deletes the element it's called on.
β Sample solution
const form = document.getElementById('task-form');
const input = document.getElementById('task-input');
const list = document.getElementById('task-list');
form.addEventListener('submit', (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return; // ignore empty input
const li = document.createElement('li');
li.textContent = text; // safe: no HTML injection
li.addEventListener('click', () => li.classList.toggle('done'));
const del = document.createElement('button');
del.textContent = 'Delete';
del.addEventListener('click', (e) => {
e.stopPropagation(); // don't also toggle 'done'
li.remove();
});
li.append(' ', del);
list.append(li);
input.value = '';
input.focus();
});
Best Practices
β Do
- Configure elements fully before inserting them into the live DOM.
- Use a DocumentFragment (or one
append(...manyNodes)call) when adding several elements. - Prefer
textContentfor any text, especially data from users or APIs. - Use event delegation β one listener on the parent β for dynamically created children.
β οΈ Avoid
- Setting
innerHTMLfrom untrusted strings β it's the classic XSS hole. innerHTML +=in a loop: it re-parses and rebuilds all existing children each time, dropping their listeners.- Touching the live DOM inside a tight loop when a fragment would do it in one operation.
Event delegation is worth seeing side by side β attach one listener to the container instead of one per child:
// Instead of a listener on every buttonβ¦
document.getElementById('list').addEventListener('click', (e) => {
const btn = e.target.closest('.delete-btn');
if (btn) btn.closest('li').remove();
});
Summary & Quiz
π Key Takeaways
document.createElement()builds a node in memory; it stays invisible until inserted.- Configure content, attributes, classes, and styles before insertion.
- Insert with
append/appendChild(end),prepend(start),insertBefore, orinsertAdjacentElement(four precise positions). - Render lists by looping data; batch many inserts through a
DocumentFragment. - Prefer
textContentoverinnerHTMLto stay safe from XSS; use<template>for reusable structure.
π― Quick Quiz
Question 1: After const p = document.createElement('p'), why can't you see the paragraph on the page yet?
Question 2: You're rendering user-submitted comments into a list. Which is the safest way to set each comment's text?
Question 3: You need to add 1,000 list items efficiently. What's the recommended approach?
π Further Reading
- MDN β Document.createElement()
- MDN β Element.insertAdjacentElement()
- MDN β DocumentFragment
- MDN β The <template> element
π What's Next?
Now that you can build and place elements, the next lesson turns to changing elements that already exist β updating their text, swapping attributes, and reading the difference between an attribute and a property in Modifying Element Content and Attributes.