JavaScript DOM Manipulation
Learn JavaScript DOM manipulation: change content, edit attributes and classes, create, insert, clone, and remove elements efficiently.
JavaScript DOM (Document Object Model) manipulation is a fundamental skill for web developers. The DOM is a live, tree-shaped representation of the page that the browser builds from your HTML; manipulating it means using JavaScript to read and change that tree at runtime so the page updates without a reload.
This guide covers the four operations you reach for most often:
- Reading and changing content with
textContentandinnerHTML. - Reading and changing attributes with
getAttribute,setAttribute, andclassList. - Creating and inserting elements with
createElement,append,insertBefore, andinsertAdjacentHTML. - Removing and replacing elements with
remove()andreplaceWith().
Before you can manipulate an element you first have to find it. If you are not yet comfortable with getElementById, querySelector, and friends, read Selecting DOM Elements and Searching: getElement, querySelector first — every example below assumes you already have a reference to the node.
Changing Element Content and Attributes
Manipulating the content and attributes of DOM elements is a crucial aspect of dynamic web development. By changing the content, we can update the text or HTML within an element. By altering attributes, we can modify properties like class, id, or src. JavaScript provides powerful methods to accomplish these tasks, enabling us to create responsive, interactive web applications. Let's explore how to use innerHTML, textContent, setAttribute, and getAttribute effectively.
Changing Element Content
We can change the content of an element using the innerHTML or textContent properties.
innerHTML vs textContent
innerHTML allows us to set or get the HTML content of an element, including any HTML tags.
<!DOCTYPE html>
<html>
<head>
<title>innerHTML vs textContent</title>
<style>
.content-container {
margin: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.html-content {
color: red;
}
</style>
</head>
<body>
<div class="content-container">
<p id="content">Original paragraph content.</p>
<button id="change-innerHTML">Change using innerHTML</button>
<button id="change-textContent">Change using textContent</button>
</div>
<script>
const content = document.getElementById('content');
const innerHTMLButton = document.getElementById('change-innerHTML');
const textContentButton = document.getElementById('change-textContent');
innerHTMLButton.addEventListener('click', () => {
content.innerHTML = 'New content with <strong class="html-content">HTML</strong> tags.';
});
textContentButton.addEventListener('click', () => {
content.textContent = 'Updated paragraph content without HTML tags.';
});
</script>
</body>
</html>Explanation:
innerHTMLallows us to set or get the HTML content of an element, including any HTML tags. In the example, clicking the "Change using innerHTML" button will replace the paragraph content with new content that includes HTML tags, which changes the appearance of the text.textContentsets or gets the text content of an element without parsing HTML tags. Clicking the "Change using textContent" button will replace the paragraph content with plain text, ignoring any HTML tags.
Use textContent when inserting user-generated content to avoid security risks like XSS (Cross-Site Scripting). Assigning a raw string from a user to innerHTML lets an attacker inject <script> or event-handler attributes that run in your page. textContent never parses HTML, so the string is shown verbatim and is always safe.
There is also a third property, outerHTML, which represents the element and its own tag. Assigning to it replaces the element entirely:
// Replace the whole <p> with an <h2>, keeping it in the same position
const p = document.querySelector('p');
p.outerHTML = '<h2>A heading instead</h2>';
// After this, `p` still points at the old, detached node — re-query if you need the new one.Changing Element Attributes
We can change the attributes of an element using the setAttribute method and retrieve them using the getAttribute method. These methods are useful for modifying elements dynamically based on user interactions or other events.
<!DOCTYPE html>
<html>
<head>
<title>setAttribute and getAttribute</title>
</head>
<body>
<div id="container" class="initial-class">Container content</div>
<button id="change-attribute">Change Attribute</button>
<button id="get-attribute">Get Attribute</button>
<script>
const container = document.getElementById('container');
const changeAttributeButton = document.getElementById('change-attribute');
const getAttributeButton = document.getElementById('get-attribute');
changeAttributeButton.addEventListener('click', () => {
container.setAttribute('class', 'new-class');
alert('Class attribute changed to "new-class"');
});
getAttributeButton.addEventListener('click', () => {
const className = container.getAttribute('class');
alert(`Current class attribute: ${className}`);
});
</script>
</body>
</html>Explanation:
setAttribute('attributeName', 'value'): This method allows us to set a new value for a specified attribute of an element. In the example, clicking the "Change Attribute" button changes the class attribute of the<div>from "initial-class" to "new-class".getAttribute('attributeName'): This method retrieves the current value of the specified attribute. Clicking the "Get Attribute" button shows an alert with the current class attribute value of the<div>.
Always use setAttribute and getAttribute for custom attributes and dynamically generated attributes. For class manipulation, prefer the classList API below — it edits classes individually instead of overwriting the whole class string.
Attributes vs. properties
A common source of confusion: an HTML attribute (what is written in the markup) is not always the same as the DOM property (the live value on the JavaScript object). getAttribute('value') reads the original markup value, while input.value reads what the user has currently typed. For boolean attributes the gap is sharper — checkbox.getAttribute('checked') reflects the markup, while checkbox.checked reflects the live state. As a rule: use properties (el.id, el.value, el.checked) for standard, frequently-changing values, and setAttribute/getAttribute for custom data- attributes or one-off markup attributes.
For reading and writing data-* attributes there is a dedicated, ergonomic API — the dataset property:
// <div id="card" data-user-id="42" data-role="admin"></div>
const card = document.getElementById('card');
console.log(card.dataset.userId); // "42" (note: data-user-id → userId)
card.dataset.role = 'editor'; // writes data-role="editor"Managing classes with classList
classList toggles individual classes without disturbing the others already present:
element.classList.add('new-class'); // add one (or several) classes
element.classList.remove('old-class'); // remove a class
element.classList.toggle('active'); // add if absent, remove if present
element.classList.replace('open', 'shut'); // swap one class for another
console.log(element.classList.contains('active')); // true / falsetoggle accepts an optional second argument to force a state, which is handy for syncing a class to a condition: el.classList.toggle('valid', isValid). For deeper styling work — including reading and writing the style property — see Working with Styles in the DOM.
Adding and Removing Elements
We can add new elements to the DOM or remove existing elements.
Adding Elements
To add new elements to the DOM, we first create them using the createElement method, then append them to an existing element using appendChild or insert them at a specific position using insertBefore.
createElement(), appendChild(), insertBefore()
<!DOCTYPE html>
<html>
<head>
<title>Adding Elements</title>
</head>
<body>
<div id="task-list">
<h2>Task List</h2>
<ul id="tasks">
<li>Initial task</li>
</ul>
<input type="text" id="new-task" placeholder="New task">
<button id="add-task">Add Task</button>
<button id="insert-before">Insert Before First Task</button>
</div>
<script>
const taskList = document.getElementById('tasks');
const newTaskInput = document.getElementById('new-task');
const addTaskButton = document.getElementById('add-task');
const insertBeforeButton = document.getElementById('insert-before');
addTaskButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
taskList.appendChild(newTask);
newTaskInput.value = '';
}
});
insertBeforeButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
const firstTask = taskList.firstElementChild;
taskList.insertBefore(newTask, firstTask);
}
});
</script>
</body>
</html>Explanation:
createElement('tagName'): This method creates a new element specified bytagName. For instance,document.createElement('li')creates a new<li>element.appendChild(newElement): This method appends a new child element to a specified parent element. In the example, clicking the "Add Task" button creates a new list item (<li>) and appends it to the task list (<ul>).insertBefore(newElement, referenceElement): This method inserts a new element before a specified reference element within the same parent. Clicking the "Insert Before First Task" button creates a new list item (<li>) and inserts it before the first task in the list.
For inserting HTML strings directly, consider insertAdjacentHTML(). To replace an existing element, element.replaceWith(newElement) is a modern alternative to combining remove() and appendChild().
insertAdjacentHTML and the modern insertion methods
When you already have an HTML string (rather than a node you built with createElement), insertAdjacentHTML(position, html) parses and inserts it in one call. The position argument is one of four keywords relative to the element:
// Given: <div id="box">content</div>
const box = document.getElementById('box');
box.insertAdjacentHTML('beforebegin', '<p>before the box</p>'); // before <div>
box.insertAdjacentHTML('afterbegin', '<p>first child</p>'); // inside, at the start
box.insertAdjacentHTML('beforeend', '<p>last child</p>'); // inside, at the end
box.insertAdjacentHTML('afterend', '<p>after the box</p>'); // after </div>Modern browsers also provide append(), prepend(), before(), and after(), which accept either nodes or plain strings and can take several arguments at once — they are usually clearer than appendChild/insertBefore:
const list = document.getElementById('tasks');
const item = document.createElement('li');
item.textContent = 'New task';
list.append(item, 'some trailing text'); // append node + string together
list.prepend('Top of the list'); // insert at the startCloning elements
To duplicate an existing node instead of building one from scratch, use cloneNode(deep). Pass true to copy the element together with all of its descendants; pass false (or nothing) to copy only the element itself:
const template = document.getElementById('card');
const copy = template.cloneNode(true); // deep clone, including children
copy.id = 'card-2'; // ids must stay unique
document.body.append(copy);For repeated, complex markup the <template> element is the purpose-built tool — its contents are inert until you clone them in.
Removing Elements
To remove an element, we can use the modern element.remove() method.
<!DOCTYPE html>
<html>
<head>
<title>Removing Elements</title>
</head>
<body>
<div id="container">
<p id="paragraph">This is a paragraph.</p>
<button id="remove-paragraph">Remove Paragraph</button>
</div>
<script>
const container = document.getElementById('container');
const paragraph = document.getElementById('paragraph');
const removeParagraphButton = document.getElementById('remove-paragraph');
removeParagraphButton.addEventListener('click', () => {
paragraph.remove();
});
</script>
</body>
</html>Explanation:
element.remove(): This modern method removes a specified element from the DOM directly. In the example, clicking the "Remove Paragraph" button removes the<p>element from the page.
Using element.remove() is the recommended approach for dynamic content changes, such as removing items from a shopping cart, as it provides cleaner syntax than the legacy parent.removeChild(child) method (which is still useful when you need a reference to the removed node).
Inserting Many Elements Efficiently
Each time you insert a node into the live document, the browser may have to recalculate layout and repaint. Doing that inside a loop — once per item — is wasteful. A DocumentFragment lets you assemble nodes off-screen and insert them all in a single operation:
const list = document.getElementById('tasks');
const fragment = document.createDocumentFragment();
for (let i = 1; i <= 1000; i++) {
const li = document.createElement('li');
li.textContent = `Task ${i}`;
fragment.appendChild(li); // no layout work — fragment is not in the document
}
list.appendChild(fragment); // one insertion, one reflowTwo related rules of thumb keep DOM-heavy code fast: batch your reads and writes (don't interleave reading offsetHeight with setting styles, or you force repeated reflows), and prefer building a string and assigning innerHTML once over many small appendChild calls when you are not attaching event listeners to each node. For an in-depth treatment, see DOM Performance Optimization.
Example: Dynamic To-Do List
Let's create a simple to-do list application to demonstrate the above concepts.
<!DOCTYPE html>
<html>
<head>
<title>To-Do List</title>
</head>
<body>
<div id="todo-list">
<h2>My To-Do List</h2>
<ul id="tasks">
<li>Learn JavaScript</li>
</ul>
<input type="text" id="new-task" placeholder="New task">
<button id="add-task">Add Task</button>
</div>
<script>
const taskList = document.getElementById('tasks');
const newTaskInput = document.getElementById('new-task');
const addTaskButton = document.getElementById('add-task');
addTaskButton.addEventListener('click', () => {
const newTaskText = newTaskInput.value;
if (newTaskText.trim()) {
const newTask = document.createElement('li');
newTask.textContent = newTaskText;
taskList.appendChild(newTask);
newTaskInput.value = '';
}
});
taskList.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
event.target.remove();
}
});
</script>
</body>
</html>Explanation:
createElementcreates a new list item element for the task, andappendChildadds it to the list.- A single click listener is attached to the
<ul>, not to each<li>. Because clicks bubble up from the clicked item to its ancestors, the listener inspectsevent.targetto decide which task was clicked and removes it. This pattern is called event delegation, and it is the reason the handler keeps working for tasks that did not exist when the page loaded.
Event delegation is why we do not add a separate listener every time a task is created — one listener on the parent handles current and future children. To go deeper on bubbling, event.target vs event.currentTarget, and delegation, read Event Handling in the DOM.
When an interface grows to many independent, frequently-updating pieces of state, manually keeping the DOM in sync becomes error-prone. Frameworks such as React, Vue, or Svelte let you describe what the UI should look like for a given state and handle the DOM updates for you. The hand-written techniques in this chapter remain the foundation those frameworks are built on.
Conclusion
Mastering DOM manipulation is essential for creating dynamic and interactive web applications. You now know how to change content (textContent, innerHTML, outerHTML), work with attributes and classes (setAttribute, dataset, classList), create and insert nodes (createElement, append, insertAdjacentHTML, cloneNode), remove and replace them (remove(), replaceWith()), and batch insertions efficiently with a DocumentFragment.
To keep building on these foundations:
- Selecting DOM Elements — find the nodes you want to change.
- Traversing the DOM — move between parents, children, and siblings.
- Working with Styles in the DOM — read and write CSS from JavaScript.
- Event Handling in the DOM — respond to user interaction.