Skip to main content

πŸ—οΈ Creating and Removing Elements

So far you've found and changed elements that already existed in the HTML. Now you become the architect: building brand-new nodes in JavaScript, slotting them precisely into the page, and tearing them down again. This is how a search box fills with results, a cart gains items, and a feed grows as you scroll.

Week 2 · Day 2 (Tuesday: DOM Manipulation) · Lecture 3

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create elements and text nodes with createElement and createTextNode
  • Assemble a multi-part component and insert it into the page
  • Choose the right insertion method: append, prepend, before, after, and insertAdjacentHTML
  • Batch many insertions efficiently with a DocumentFragment
  • Clone reusable markup from a <template> with cloneNode
  • Remove elements cleanly with remove() and understand why event delegation matters

Estimated Time: 65 minutes

Practice: Build a small list that adds and removes items dynamically.

In This Lesson

The Build-Insert-Remove Cycle

Dynamic UI follows a simple rhythm. You create a node in memory, configure it (text, classes, attributes), insert it into the live page where the user can see it, and eventually remove it when it's no longer needed. A node you've created but not yet inserted exists only in JavaScript β€” it isn't on screen until you attach it to the document.

graph TB A[document] --> B[createElement] B --> C[Configure: text, class, attrs] C --> D[Insert: append / before / after] A --> E[createDocumentFragment] E --> F[Add many elements off-screen] F --> D D --> G[Live DOM β€” visible] G --> H[remove]

Understanding that "created but not attached" limbo is the key insight of this lesson: it's exactly what makes DocumentFragment and templates fast.

Creating Elements

document.createElement(tag) makes a fresh, empty element in memory. You then set its content and properties before inserting it.

// Create and configure a single element:
const div = document.createElement('div');
div.textContent = 'Hello, World!';
div.className = 'message';
div.id = 'welcome-message';

// Nothing is on screen yet β€” div lives only in memory
// until you attach it to the document (next section).

// Build a small nested structure by assembling children:
const card = document.createElement('div');
card.className = 'card';

const header = document.createElement('div');
header.className = 'card-header';
header.textContent = 'Card Title';

const body = document.createElement('div');
body.className = 'card-body';
body.textContent = 'Card content goes here';

card.append(header, body);   // card now contains both children

// Pure text (no element) via createTextNode:
const p = document.createElement('p');
p.append(document.createTextNode('This is plain text'));

A reusable component factory

Wrapping element creation in a function gives you a repeatable "component." Note we use textContent and properties β€” never innerHTML with data β€” so the card is safe even if product.name came from a user.

function createProductCard(product) {
    const card = document.createElement('div');
    card.className = 'product-card';
    card.dataset.productId = product.id;

    const title = document.createElement('h3');
    title.className = 'product-title';
    title.textContent = product.name;              // safe

    const price = document.createElement('div');
    price.className = 'product-price';
    price.textContent = `$${product.price.toFixed(2)}`;

    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'add-to-cart';
    button.textContent = 'Add to Cart';
    button.addEventListener('click', () => addToCart(product.id));

    card.append(title, price, button);             // one call, many children
    return card;
}

// Usage:
const card = createProductCard({ id: 1, name: 'Headphones', price: 99.99 });
document.querySelector('.products-container').append(card);

πŸ’‘ append() beats appendChild()

The modern append() accepts multiple nodes and plain strings in one call: card.append(title, price, 'text'). The older appendChild() takes exactly one node and returns it. Prefer append() unless you specifically need that return value.

Inserting into the DOM

Creating an element is inert until you place it somewhere. Modern DOM gives you an intuitive set of positioning methods.

const container = document.querySelector('.container');
const el = document.createElement('div');
el.textContent = 'New Element';

// The four modern positioners:
container.append(el);    // as the LAST child of container
container.prepend(el);   // as the FIRST child of container
target.before(el);       // as a sibling, just BEFORE target
target.after(el);        // as a sibling, just AFTER target

// Replace an element entirely:
oldElement.replaceWith(el);

Precise placement with insertAdjacent…

When you need fine control relative to one element, the insertAdjacent* family takes a position keyword. The four keywords map onto the element like this:

<!-- beforebegin -->
<div class="target">
    <!-- afterbegin -->
    … existing content …
    <!-- beforeend -->
</div>
<!-- afterend -->
const target = document.querySelector('.target');

target.insertAdjacentElement('beforeend', el);   // an element node
target.insertAdjacentText('afterbegin', 'Hi');   // plain text

// insertAdjacentHTML parses a string β€” SAFE only with trusted markup:
target.insertAdjacentHTML('beforeend', '<span class="badge">New</span>');

⚠️ insertAdjacentHTML carries the same XSS risk as innerHTML

It parses its string as HTML, so never build it from untrusted input. For user data, create elements and set textContent instead.

Batching with DocumentFragment

Every time you insert into the live DOM, the browser may recalculate layout β€” a "reflow." Insert a thousand items one at a time and you trigger up to a thousand reflows. A DocumentFragment is a lightweight, off-screen container: you fill it cheaply in memory, then attach it once, causing a single reflow.

// ❌ Slow β€” inserts inside the loop, reflow after reflow:
for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    document.querySelector('.list').append(div);   // touches live DOM 1000Γ—
}

// βœ… Fast β€” build off-screen, attach once:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    fragment.append(div);                          // in memory, no reflow
}
document.querySelector('.list').append(fragment);  // ONE reflow
A DocumentFragment collects nodes off-screen and inserts them in one reflow DocumentFragment (off-screen) fill cheaply β€” no reflow append once Live DOM all nodes land together βœ… a single reflow
Assemble many nodes in a fragment, then insert the whole batch at once.

Cloning Templates

When you need the same structure over and over, building it element-by-element each time is tedious. The <template> element lets you write the markup once in HTML β€” inert, never rendered β€” then stamp out copies with cloneNode(true).

<!-- In your HTML β€” this never renders on its own: -->
<template id="item-template">
    <li class="list-item">
        <span class="item-name" data-field="name"></span>
        <button class="delete-btn">Delete</button>
    </li>
</template>
const template = document.getElementById('item-template');

function createItem(data) {
    // cloneNode(true) = deep copy, including all descendants:
    const clone = template.content.cloneNode(true);

    clone.querySelector('[data-field="name"]').textContent = data.name;
    clone.querySelector('.delete-btn')
         .addEventListener('click', () => console.log('delete', data.id));

    return clone;   // a fragment ready to append
}

document.querySelector('.list').append(createItem({ id: 1, name: 'Milk' }));

βœ… Templates keep structure in HTML, logic in JS

Instead of hiding markup inside long JavaScript strings, the shape lives in your HTML where it's easy to read and edit, and your script only fills in the data. It's cleaner and safer than assembling HTML strings by hand.

Removing Elements

Taking a node out of the page is refreshingly simple with the modern API.

// Remove an element directly β€” no parent reference needed:
document.querySelector('.removable').remove();

// Remove every child of a container:
function clearChildren(parent) {
    while (parent.firstChild) {
        parent.firstChild.remove();
    }
}
// (Setting parent.innerHTML = '' also clears it, but discards any
//  event listeners and is less explicit about intent.)

// Remove all elements matching a condition:
function removeWhere(selector, predicate) {
    document.querySelectorAll(selector).forEach(el => {
        if (predicate(el)) el.remove();
    });
}
removeWhere('.item', el => el.dataset.expired === 'true');

Removing with a fade-out

function removeWithFade(element, duration = 300) {
    element.style.transition = `opacity ${duration}ms`;
    element.style.opacity = '0';
    // Wait for the transition to finish, THEN remove:
    element.addEventListener('transitionend', () => element.remove(), { once: true });
}

⚠️ Listeners on removed nodes, and why delegation wins

When you remove an element, any event listeners attached directly to it are discarded with it β€” good. But if you constantly add and remove items, attaching a fresh listener to every one is wasteful and easy to leak. The robust pattern is event delegation: attach a single listener to a stable parent and inspect event.target. You'll go deep on this in the next lesson.

// One listener handles clicks for ALL current AND future .delete-btn:
document.querySelector('.list').addEventListener('click', (e) => {
    if (e.target.matches('.delete-btn')) {
        e.target.closest('.list-item').remove();
    }
});

Practice & Quiz

πŸ‹οΈ Exercise 1: Render a list efficiently

Goal: Write renderList(container, items) that turns an array of strings into <li> elements inside container, using a DocumentFragment so the whole list inserts in one reflow.

function renderList(container, items) {
    // TODO: build all <li> in a fragment, then append once
}
renderList(document.querySelector('ul'), ['Alpha', 'Beta', 'Gamma']);
πŸ’‘ Hint

Create document.createDocumentFragment(). Loop the items, make an li, set textContent, append it to the fragment. After the loop, container.append(fragment) once.

βœ… Solution
function renderList(container, items) {
    const fragment = document.createDocumentFragment();
    for (const text of items) {
        const li = document.createElement('li');
        li.textContent = text;
        fragment.append(li);
    }
    container.append(fragment);   // single reflow
}

πŸ‹οΈ Exercise 2: An add/remove todo strip

Goal: Write addTodo(listEl, text) that appends an <li> containing the text and a Delete button which removes that <li> when clicked.

βœ… Solution
function addTodo(listEl, text) {
    const li = document.createElement('li');
    li.className = 'todo';

    const label = document.createElement('span');
    label.textContent = text;

    const del = document.createElement('button');
    del.type = 'button';
    del.textContent = 'Delete';
    del.addEventListener('click', () => li.remove());

    li.append(label, del);
    listEl.append(li);
    return li;
}

🎯 Quick Quiz

Question 1: After const d = document.createElement('div'), is d visible on the page?

Question 2: Why insert 1,000 elements through a DocumentFragment instead of one at a time?

Question 3: You add and remove list items constantly. What's the most robust way to handle their click events?

Best Practices & Pitfalls

βœ… Do

  • Use a DocumentFragment (or one big string built safely) when inserting many nodes at once
  • Clone a <template> for repeated structures instead of rebuilding them by hand
  • Set data with textContent and properties, keeping user input out of parsed HTML
  • Prefer element.remove() β€” no parent reference needed
  • Use event delegation for collections that grow and shrink

❌ Don't

  • Insert inside a tight loop directly into the live DOM β€” batch instead
  • Build HTML strings from untrusted input for innerHTML/insertAdjacentHTML
  • Forget that a created element does nothing until it's inserted
  • Attach a brand-new listener to every dynamically created element when delegation would do

βœ… Clone from a template instead of hand-building

// ❌ Rebuild every element, every time:
function makeCard(data) {
    const card = document.createElement('div');
    const title = document.createElement('h3');
    // …many more lines…
}

// βœ… Stamp a copy of markup written once in HTML:
const template = document.getElementById('card-template');
function makeCard(data) {
    const clone = template.content.cloneNode(true);
    clone.querySelector('.title').textContent = data.title;
    return clone;
}

Summary

πŸŽ‰ Key Takeaways

  • createElement builds a node in memory; it's invisible until you insert it
  • Insert with the modern quartet: append, prepend, before, after β€” plus insertAdjacent* for fine control
  • A DocumentFragment batches many insertions into a single reflow
  • Clone a <template> with cloneNode(true) for repeated structures
  • element.remove() takes a node out cleanly, no parent needed
  • Use event delegation so one listener serves an ever-changing list

πŸ“š Additional Resources

πŸš€ What's Next?

You can now build, place, and remove any part of the page. The final piece of interactivity is making it all respond to the user: Event Listeners and Handlers β€” clicks, input, keyboard, the event object, and the delegation pattern we just previewed.

πŸŽ‰ You're a DOM architect now!

Select, modify, create, remove β€” you hold every tool to reshape a page. Next, you'll wire it up to the user.