Dynamic Content
✨ Making Pages Come Alive
Think about the last time you used a to-do app. You typed a task, pressed Enter, and it appeared in the list. You clicked a delete button and it vanished. You dragged it to reorder. At no point did the page reload.
All of that happens because JavaScript can create, insert, update, and remove HTML elements on the fly. This is dynamic content — and it’s the technique that powers every modern web application, from social media feeds to shopping carts to dashboards.
In this lesson you’ll learn to build elements from scratch, batch them efficiently, and manage lists of dynamic content without performance pitfalls.
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:
| Method | Where it inserts | Example |
|---|---|---|
parent.append(el) | Inside parent, after last child | Add item to end of list |
parent.prepend(el) | Inside parent, before first child | Add item to start of list |
sibling.before(el) | Right before the sibling | Insert row above current |
sibling.after(el) | Right after the sibling | Insert row below current |
el.replaceWith(newEl) | Replaces el entirely | Swap 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?
- What is the difference between append() and appendChild()?
- A newly created element with createElement() is not visible on the page. Why?
- What is the difference between prepend() and before()?
Check Your Answers
- 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.
- 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.
- 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">×</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?
- Why is DocumentFragment better than appending elements one by one?
- Why should you use event delegation for delete buttons on dynamic list items?
- Why is textContent safer than innerHTML for user-provided content?
Check Your Answers
- 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.
- 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.
- 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?
- When should you update elements in place vs replace them entirely?
- What advantage does the HTML <template> element have over creating a template with createElement?
- What does cloneNode(true) do differently from cloneNode(false)?
Check Your Answers
- 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.
- 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.
- 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?
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?
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?
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?
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?
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:
| Criteria | You've succeeded if... |
|---|---|
| Elements are created with createElement | No innerHTML with user data — textContent is used for user input |
| DocumentFragment used for batch rendering | Initial render uses a fragment, not individual appends |
| Event delegation on columns | One listener per column handles all task buttons |
| Tasks move between columns | Forward/back buttons move the task element to the correct column |
| Counts update dynamically | Column 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:
- MDN: Document.createElement() - Reference for creating DOM elements