Beginner45 minutesJavaScriptDOM

Dynamic Content

Learning Objectives

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

  • Create Create new DOM elements with document.createElement()
  • Set attributes, classes, and text content on created elements
  • Insert elements at specific positions using append, prepend, before, and after
  • Use DocumentFragment to batch multiple insertions for better performance
  • Build dynamic lists with add and delete functionality
  • Use template literals to generate HTML strings safely
  • Clone elements with cloneNode() for repeating patterns
  • Apply Apply best practices for updating the DOM efficiently

Creating Elements

The document.createElement() method creates a new HTML element in memory. It doesn’t appear on the page until you insert it into the DOM.

// Step 1: Create the element
const card = document.createElement('div');

// Step 2: Configure it
card.className = 'card';
card.id = 'user-card-1';
card.setAttribute('data-user-id', '42');

// Step 3: Add content
card.textContent = 'Hello, World!';
// Or use innerHTML for HTML content:
// card.innerHTML = '<h3>Hello</h3><p>World</p>';

// Step 4: Insert into the page
document.querySelector('.container').appendChild(card);

Building Complex Elements

For elements with child nodes, create each piece separately and assemble them:

function createUserCard(user) {
  const card = document.createElement('article');
  card.className = 'user-card';

  const heading = document.createElement('h3');
  heading.textContent = user.name;

  const email = document.createElement('p');
  email.textContent = user.email;

  const deleteBtn = document.createElement('button');
  deleteBtn.textContent = 'Delete';
  deleteBtn.className = 'btn-delete';

  card.append(heading, email, deleteBtn);
  return card;
}

append vs appendChild: append() accepts multiple arguments and can insert text strings. appendChild() only accepts one node. Prefer append() for modern code.

Insertion Methods

Modern JavaScript gives you precise control over where a new element lands relative to an existing one:

MethodWhere it insertsExample
parent.append(el)Inside parent, after last childAdd item to end of list
parent.prepend(el)Inside parent, before first childAdd item to start of list
sibling.before(el)Right before the siblingInsert row above current
sibling.after(el)Right after the siblingInsert row below current
el.replaceWith(newEl)Replaces el entirelySwap a loading spinner for content
const list = document.querySelector('ul');
const newItem = document.createElement('li');
newItem.textContent = 'New task';

// Add to the end
list.append(newItem);

// Add to the beginning
list.prepend(newItem);

// Insert before a specific item
const thirdItem = list.children[2];
thirdItem.before(newItem);

⏸️ Pause & Check: Do You Understand?

Before moving forward, can you answer these?

  1. What is the difference between append() and appendChild()?
  2. A newly created element with createElement() is not visible on the page. Why?
  3. What is the difference between prepend() and before()?
Check Your Answers
  1. append() can take multiple arguments and accepts both nodes and text strings. appendChild() takes only one node argument. append() is the modern, more flexible method.
  2. createElement() creates the element in memory only. You must insert it into the DOM using a method like append(), prepend(), before(), after(), or appendChild() for it to appear.
  3. prepend() inserts inside the target element as its first child. before() inserts as a sibling immediately before the target element. They place content at different levels of the DOM tree.

How confident are you with this concept?

😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!

DocumentFragment: Batch Insertions

Every time you insert an element into the DOM, the browser recalculates layout and repaints. If you’re adding 50 items one by one, that’s 50 recalculations. A DocumentFragment lets you build everything in memory first, then insert it all at once — only one recalculation.

Slow: One by One

// ❌ 100 separate DOM updates
const list = document.querySelector('ul');

users.forEach(user => {
  const li = document.createElement('li');
  li.textContent = user.name;
  list.appendChild(li);
});

Fast: Batch with Fragment

// ✅ 1 DOM update
const list = document.querySelector('ul');
const fragment = document.createDocumentFragment();

users.forEach(user => {
  const li = document.createElement('li');
  li.textContent = user.name;
  fragment.appendChild(li);
});

list.appendChild(fragment);

Performance rule of thumb: If you’re adding more than 3–5 elements at once, use a DocumentFragment. The browser only does one layout calculation instead of one per element.

Building Dynamic Lists

The most common dynamic content pattern is a list that users can add to and delete from. Here’s a complete, production-quality pattern:

const form = document.querySelector('#todo-form');
const input = document.querySelector('#todo-input');
const list = document.querySelector('#todo-list');

// Add a new item on form submit
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;

  const li = document.createElement('li');
  li.className = 'todo-item';
  li.innerHTML = `
    <span class="todo-text">${text}</span>
    <button class="btn-delete" aria-label="Delete">&times;</button>
  `;

  list.appendChild(li);
  input.value = '';
  input.focus();
});

// Delete items using event delegation
list.addEventListener('click', (e) => {
  if (e.target.matches('.btn-delete')) {
    e.target.closest('li').remove();
  }
});

Security note: When using innerHTML with user input, always sanitise the input first to prevent XSS attacks. For plain text, prefer textContent which is always safe. The example above uses innerHTML only for the predefined button markup — the user text should be escaped or set via textContent on the span separately.

⏸️ Pause & Check: Do You Understand?

Before moving forward, can you answer these?

  1. Why is DocumentFragment better than appending elements one by one?
  2. Why should you use event delegation for delete buttons on dynamic list items?
  3. Why is textContent safer than innerHTML for user-provided content?
Check Your Answers
  1. Each append to the live DOM triggers a layout recalculation and repaint. DocumentFragment collects all elements in memory first, then inserts them all in one operation — only one recalculation happens.
  2. Event delegation uses one listener on the parent, so it automatically works for items added after the listener was set up. Without it, you’d need to add a new listener every time you create an item.
  3. textContent treats everything as plain text, so HTML and script tags are displayed as text. innerHTML parses and executes HTML, which can lead to cross-site scripting (XSS) attacks if the content comes from a user.

How confident are you with this concept?

😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!

Cloning Elements

When you need multiple copies of a complex element, cloneNode() is faster than calling createElement() for each piece:

// Create a template element once
const template = document.createElement('div');
template.className = 'card';
template.innerHTML = `
  <img class="card-image" src="" alt="">
  <div class="card-body">
    <h3 class="card-title"></h3>
    <p class="card-text"></p>
  </div>
`;

// Clone it for each item (true = deep clone with children)
products.forEach(product => {
  const card = template.cloneNode(true);

  card.querySelector('.card-image').src = product.image;
  card.querySelector('.card-image').alt = product.name;
  card.querySelector('.card-title').textContent = product.name;
  card.querySelector('.card-text').textContent = product.description;

  container.appendChild(card);
});

Using HTML <template> Elements

HTML has a built-in <template> element designed exactly for this pattern. Its contents are parsed but not rendered until you clone them:

// In your HTML:
// <template id="card-template">
//   <div class="card">...</div>
// </template>

const template = document.querySelector('#card-template');

products.forEach(product => {
  // Clone the template content
  const clone = template.content.cloneNode(true);

  // Fill in the data
  clone.querySelector('.card-title').textContent = product.name;

  container.appendChild(clone);
});

Updating vs Replacing Content

When data changes, you have two options: update existing elements in place, or tear everything down and rebuild. Each approach has trade-offs:

Update in Place

  • Preserves scroll position and focus
  • Smooth transitions possible
  • More complex code
  • Must track what changed

Replace Entirely

  • Simpler code
  • Guaranteed correct state
  • Loses scroll position and focus
  • Can be slower for large lists

Update in Place Example

function updatePrice(productId, newPrice) {
  const card = document.querySelector(`[data-id="${productId}"]`);
  if (!card) return;

  const priceEl = card.querySelector('.price');
  priceEl.textContent = `$${newPrice.toFixed(2)}`;
  priceEl.classList.add('price-updated');
}

Replace Entirely Example

function renderProducts(products) {
  const container = document.querySelector('.products');
  const fragment = document.createDocumentFragment();

  products.forEach(product => {
    fragment.appendChild(createProductCard(product));
  });

  // Clear and replace in one go
  container.innerHTML = '';
  container.appendChild(fragment);
}

Real-World Example: Comment Section

Let’s put it all together with a comment section that creates, renders, and deletes comments dynamically:

const commentForm = document.querySelector('#comment-form');
const commentList = document.querySelector('#comments');
const commentCount = document.querySelector('#comment-count');

function createComment(text, author) {
  const comment = document.createElement('article');
  comment.className = 'comment';

  const header = document.createElement('header');
  const nameEl = document.createElement('strong');
  nameEl.textContent = author;
  const timeEl = document.createElement('time');
  timeEl.textContent = new Date().toLocaleString();
  header.append(nameEl, ' \u2014 ', timeEl);

  const body = document.createElement('p');
  body.textContent = text;

  const deleteBtn = document.createElement('button');
  deleteBtn.textContent = 'Delete';
  deleteBtn.className = 'btn-delete';

  comment.append(header, body, deleteBtn);
  return comment;
}

function updateCount() {
  commentCount.textContent = commentList.children.length;
}

// Add comment
commentForm.addEventListener('submit', (e) => {
  e.preventDefault();
  const text = commentForm.querySelector('textarea').value.trim();
  if (!text) return;

  commentList.prepend(createComment(text, 'You'));
  updateCount();
  commentForm.reset();
});

// Delete comment (event delegation)
commentList.addEventListener('click', (e) => {
  if (e.target.matches('.btn-delete')) {
    e.target.closest('.comment').remove();
    updateCount();
  }
});

Notice the patterns: We use createElement + textContent (safe from XSS), prepend to add newest first, event delegation for delete buttons, and closest() to find the parent comment from the button click.

⏸️ Pause & Check: Do You Understand?

Before moving forward, can you answer these?

  1. When should you update elements in place vs replace them entirely?
  2. What advantage does the HTML <template> element have over creating a template with createElement?
  3. What does cloneNode(true) do differently from cloneNode(false)?
Check Your Answers
  1. Update in place when you need to preserve scroll position, focus, or run smooth transitions. Replace entirely when the data has changed so much that updating individual elements would be more complex than rebuilding.
  2. The <template> element’s content is parsed by the browser but never rendered, so it’s validated HTML. It also keeps the structure in the HTML file where designers and other developers can easily see and edit it.
  3. cloneNode(true) creates a deep clone — the element and all its children. cloneNode(false) only clones the element itself, without any of its child nodes or content.

How confident are you with this concept?

😕 Still confused | 🤔 Getting there | 😊 Got it! | 🎉 Could explain it to a friend!

Guided Practice: Dynamic Product Gallery

Build a Dynamic Product Gallery

Create the HTML structure

Create a file called gallery.html with a product form and gallery container:




  
  Product Gallery
  


  

Product Gallery (0 products)

💡 Need a hint?
Notice the <template> element — its content is invisible but ready to be cloned.

Render initial products with DocumentFragment

In gallery.js, start with sample data and render it using a fragment:

const gallery = document.querySelector('.gallery');
const template = document.querySelector('#card-template');
const countEl = document.querySelector('.product-count');

const sampleProducts = [
  { name: 'Wireless Mouse', price: 29.99 },
  { name: 'Mechanical Keyboard', price: 89.99 },
  { name: 'USB-C Hub', price: 49.99 },
];

function renderProducts(products) {
  const fragment = document.createDocumentFragment();
  products.forEach(p => fragment.appendChild(createCard(p)));
  gallery.appendChild(fragment);
  updateCount();
}

renderProducts(sampleProducts);
💡 Need a hint?
The fragment collects all three cards, then inserts them in one DOM operation.

Create cards by cloning the template

Write the createCard function that clones the template and fills in data:

function createCard(product) {
  const clone = template.content.cloneNode(true);
  clone.querySelector('.card-name').textContent = product.name;
  clone.querySelector('.card-price').textContent = `$${product.price.toFixed(2)}`;
  return clone;
}
💡 Need a hint?
cloneNode(true) does a deep clone of the template content. We use textContent so user data is never parsed as HTML.

Add new products from the form

Listen for form submissions and create new cards:

const form = document.querySelector('#product-form');

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const data = new FormData(form);
  const product = {
    name: data.get('name'),
    price: parseFloat(data.get('price'))
  };
  gallery.appendChild(createCard(product));
  updateCount();
  form.reset();
});
💡 Need a hint?
FormData is a clean way to read form values. We reuse the same createCard function.

Delete products with event delegation and update the count

Add one click listener on the gallery for all delete buttons, and write the count updater:

gallery.addEventListener('click', (e) => {
  if (e.target.matches('.btn-delete')) {
    e.target.closest('.product-card').remove();
    updateCount();
  }
});

function updateCount() {
  countEl.textContent = `(${gallery.children.length} products)`;
}
💡 Need a hint?
Event delegation means this one listener works for existing and future cards. closest() traverses from the button to its card.

You're on track if you can:

  • ☐ Products render from a data array using DocumentFragment
  • ☐ New products can be added via a form
  • ☐ Products can be deleted with event delegation
  • ☐ A counter updates automatically when products are added or removed
  • ☐ HTML template element is used for the card structure

Independent Practice

💪 Independent Challenge

Now try this on your own without hints!

Your Task:

Build a dynamic task board (like a simplified Trello). The board should have three columns: To Do, In Progress, and Done. Users should be able to add tasks, move them between columns, and delete them \u2014 all without page reloads.

Requirements:
  • Three columns rendered dynamically from data
  • Add new tasks to the To Do column via a form
  • Move tasks between columns using forward/back buttons on each task
  • Delete tasks with a delete button
  • Use DocumentFragment when initially rendering tasks
  • Use event delegation for all button clicks
  • Display task counts for each column that update automatically
Stretch Goals (Optional):
  • Persist tasks to localStorage so they survive page reloads
  • Add drag-and-drop to move tasks between columns
  • Add a filter input that shows/hides tasks by keyword
  • Add colour-coded priority labels to tasks

Success Criteria:

CriteriaYou've succeeded if...
Elements are created with createElementNo innerHTML with user data — textContent is used for user input
DocumentFragment used for batch renderingInitial render uses a fragment, not individual appends
Event delegation on columnsOne listener per column handles all task buttons
Tasks move between columnsForward/back buttons move the task element to the correct column
Counts update dynamicallyColumn headers show current task count, updated on every add/move/delete

Lesson Complete: What You Learned

Key Takeaways:

  • Use createElement() + textContent for safe, secure element creation
  • Modern insertion methods (append, prepend, before, after) give precise placement control
  • DocumentFragment batches multiple insertions into one DOM update for better performance
  • The HTML <template> element and cloneNode() are ideal for repeating patterns
  • Event delegation on a stable parent works with dynamically added elements
  • Choose between updating in place (preserves state) and replacing entirely (simpler code) based on your needs

Learning Objectives Review:

Look back at what you set out to learn. Can you now:

  • ✅ Create new DOM elements with document.createElement() Check!
  • ✅ Set attributes, classes, and text content on created elements Got it!
  • ✅ Insert elements at specific positions using append, prepend, before, and after Can explain it!
  • ✅ Use DocumentFragment to batch multiple insertions Could teach this!
  • ✅ Build dynamic lists with add and delete functionality Check!
  • ✅ Clone elements with cloneNode() for repeating patterns Got it!
  • ✅ Choose between updating and replacing content Can explain it!

If you can confidently answer "yes" to most of these, you're ready to move on!

Think & Reflect:

💭 💭 Reflection Questions

  • Why is textContent safer than innerHTML when displaying user data?
  • When would you choose to update elements in place vs rebuild the entire list?
  • How does event delegation interact with dynamically created elements?
  • What websites do you use daily that likely create DOM elements dynamically?

🤔 Real-World Test:

Dynamic content creation is the core of every interactive web application. Social media feeds render new posts as you scroll. Shopping carts add and remove items. Chat applications append messages in real time. Dashboards update graphs and tables when new data arrives.

The patterns you learned here — createElement, DocumentFragment, event delegation, and template cloning — are the foundation that frameworks like Vue, React, and Angular build on top of. Understanding them makes you a better developer in any framework.

🎯 Looking Ahead:

Congratulations! You’ve completed the DOM Basics course. You now understand how to select elements, traverse the tree, handle events, and create dynamic content. These are the building blocks of every interactive website.

Ready for the next step? Check out the recommendations below to continue your learning journey.

Recommended Next Steps

Related Topics

Explore these related tutorials to expand your knowledge:

Additional Resources

Deepen your understanding with these helpful resources:

Progress tracking is disabled. Enable it in to track your completed tutorials.