Skip to main content

πŸ—οΈ 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() and createTextNode()
  • Configure an element's content, attributes, classes, and styles before it enters the page
  • Insert nodes precisely using appendChild, append, prepend, insertBefore, and insertAdjacentElement
  • Render lists from data and batch inserts with a DocumentFragment for performance
  • Choose safely between textContent and innerHTML to 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.

The create, configure, insert workflow Three connected stages: create a node in memory, configure its content and attributes, then insert it into a parent already in the document. 1. Create createElement() 2. Configure text Β· attrs Β· classes 3. Insert append() β†’ visible
Figure 1 β€” Create, configure, then insert. A node is invisible and inert until step 3 attaches it to a parent that lives in the document.
πŸ’‘ 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
The four insertAdjacentElement positions A reference element box with beforebegin above it, afterbegin just inside the top, beforeend just inside the bottom, and afterend below it. reference element 'beforebegin' 'afterbegin' 'beforeend' 'afterend'
Figure 2 β€” The four positions accepted by 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

  1. On form submit, create an <li> whose text is the input's value (use textContent).
  2. Give each item a "Delete" button that removes its own <li>.
  3. Clear and refocus the input after adding.
  4. Stretch: ignore empty/whitespace-only input, and clicking an item's text toggles a done class.
πŸ’‘ 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 textContent for any text, especially data from users or APIs.
  • Use event delegation β€” one listener on the parent β€” for dynamically created children.

⚠️ Avoid

  • Setting innerHTML from 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, or insertAdjacentElement (four precise positions).
  • Render lists by looping data; batch many inserts through a DocumentFragment.
  • Prefer textContent over innerHTML to 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

πŸš€ 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.