Skip to content

Creating and Modifying Elements

beginner15 min read

Building the Page with JavaScript

Selecting elements is only half the story. The real power of the DOM comes from creating, modifying, and removing elements dynamically. Every interactive feature you've ever used — a new chat message appearing, a todo item being added to a list, a modal popping open — involves DOM manipulation.

The key question is: what's the right way to do it? Because there are multiple approaches, and some are dramatically better than others depending on what you're doing.

Mental Model

Think of DOM manipulation like editing a document in Google Docs. You can add new paragraphs (createElement), copy-paste sections (cloneNode), delete paragraphs (removeChild/remove), or select-all and replace everything at once (innerHTML). The select-all approach is fast to type but destructive — it wipes out everything and rebuilds from scratch, destroying any existing state like cursor position or event listeners. The surgical approach takes more code but preserves what's already there.

Creating Elements

createElement — The Standard Way

document.createElement creates a new element node in memory. It doesn't appear on the page until you insert it into the DOM tree.

const button = document.createElement('button');
button.textContent = 'Click me';
button.className = 'btn-primary';

// Still invisible — exists only in memory
// Now insert it into the page
document.body.appendChild(button);

This two-step process (create, then insert) is intentional. It lets you fully configure an element before it touches the DOM, which avoids unnecessary reflows.

Building Complex Structures

For elements with children, you build them piece by piece:

const card = document.createElement('div');
card.className = 'card';

const title = document.createElement('h2');
title.textContent = 'Card Title';

const body = document.createElement('p');
body.textContent = 'Card description goes here.';

card.appendChild(title);
card.appendChild(body);

document.querySelector('.container').appendChild(card);

DocumentFragment — Batch Your Inserts

When you need to add many elements, inserting them one by one triggers a layout recalculation each time. A DocumentFragment is a lightweight container that lives entirely in memory. You build your structure inside it, then insert the entire fragment in a single operation.

const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const item = document.createElement('li');
  item.textContent = `Item ${i + 1}`;
  fragment.appendChild(item);
}

// One DOM insertion instead of 100
document.querySelector('ul').appendChild(fragment);
Modern browsers are smart about batching

Modern rendering engines batch DOM mutations that happen synchronously. So 100 consecutive appendChild calls in the same synchronous block often perform similarly to a DocumentFragment. But using fragments is still good practice — it makes your intent clear and guarantees batching in every engine.

Quiz
What happens when you call document.createElement('div')?

Inserting Elements

append and prepend (modern)

const container = document.querySelector('.list');

// append — adds to the end (can take multiple arguments, including strings)
container.append(newElement);
container.append('Some text', anotherElement); // accepts strings too

// prepend — adds to the beginning
container.prepend(newElement);

appendChild (classic)

// appendChild — returns the appended node
const appended = container.appendChild(newElement);

The difference: append accepts strings and multiple arguments. appendChild only accepts a single node but returns it (useful for chaining).

insertBefore

Inserts a node before a specific reference node:

const list = document.querySelector('ul');
const referenceItem = list.children[2]; // the 3rd item
const newItem = document.createElement('li');
newItem.textContent = 'Inserted before item 3';

list.insertBefore(newItem, referenceItem);

before, after, and replaceWith (modern)

These methods are called on the target element itself:

const target = document.querySelector('.target');

// Insert a sibling before
target.before(newElement);

// Insert a sibling after
target.after(newElement);

// Replace the element entirely
target.replaceWith(newElement);
Quiz
What is the key difference between append() and appendChild()?

Removing Elements

remove (modern)

The simplest way to remove an element from the DOM:

const element = document.querySelector('.old-item');
element.remove();

removeChild (classic)

The older approach requires calling removeChild on the parent:

const parent = document.querySelector('.list');
const child = parent.children[0];
parent.removeChild(child);

Cloning Elements

cloneNode creates a copy of an element. Pass true for a deep clone (includes all descendants), or false for a shallow clone (just the element itself, no children).

const original = document.querySelector('.template');

// Shallow clone — just the outer element
const shallow = original.cloneNode(false);

// Deep clone — the element and ALL its descendants
const deep = original.cloneNode(true);

document.body.appendChild(deep);
Common Trap

cloneNode copies the element's attributes and structure, but it does not copy event listeners added with addEventListener. If you clone a button that has a click handler, the clone won't have that handler. You need to re-attach listeners to cloned elements.

Modifying Content: innerHTML vs textContent vs innerText

These three properties all deal with an element's content, but they behave very differently.

textContent — Fast and Safe

Returns or sets the raw text content of an element and all its descendants. No HTML parsing.

const el = document.querySelector('.message');

// Reading — gets all text, ignores tags
el.textContent; // "Hello World" (even if "World" is inside a <strong>)

// Writing — replaces everything with plain text
el.textContent = '<b>Not bold</b>'; // renders as literal text: <b>Not bold</b>

innerText — The "Visual" Text

Similar to textContent but respects CSS visibility. It returns only the text that's actually visible on screen, and it triggers a reflow to compute visibility.

const el = document.querySelector('.content');

// innerText hides what CSS hides
// If a child has display:none, its text is excluded
el.innerText;  // only visible text

// textContent returns everything regardless of CSS
el.textContent; // all text, including hidden elements

innerHTML — Parses HTML

Reads or sets the HTML markup inside an element. When you set it, the browser parses the string as HTML and builds DOM nodes.

const container = document.querySelector('.container');

// Reading — get HTML as a string
container.innerHTML; // "<h1>Title</h1><p>Content</p>"

// Writing — replaces all content with parsed HTML
container.innerHTML = '<h2>New Title</h2><p>New content</p>';
innerHTML and security

Never use innerHTML with user-supplied data. It parses HTML, which means an attacker can inject scripts. If you're inserting text that came from a user, always use textContent instead.

// DANGEROUS — user input could contain script tags
element.innerHTML = userInput;

// SAFE — treated as plain text, no HTML parsing
element.textContent = userInput;
Quiz
You set element.textContent to a string containing an HTML bold tag. What renders on screen?

classList — Managing CSS Classes

The classList API is the modern way to add, remove, and toggle CSS classes.

const element = document.querySelector('.card');

element.classList.add('active');        // add a class
element.classList.remove('loading');    // remove a class
element.classList.toggle('expanded');   // add if missing, remove if present
element.classList.contains('active');   // check if class exists → true/false
element.classList.replace('old', 'new'); // swap one class for another

// Add multiple classes at once
element.classList.add('highlight', 'animate', 'visible');

toggle also accepts a second argument — a boolean that forces the class on or off:

// Force add (true) or force remove (false)
element.classList.toggle('dark', isDarkMode);
// Equivalent to: if (isDarkMode) add('dark') else remove('dark')

dataset — Custom Data Attributes

HTML data-* attributes are accessible in JavaScript through the dataset property. The attribute name is converted from kebab-case to camelCase.

// HTML: <div data-user-id="42" data-active="true" data-role="admin">
const el = document.querySelector('[data-user-id]');

el.dataset.userId;  // "42" (always a string)
el.dataset.active;  // "true" (string, not boolean!)
el.dataset.role;    // "admin"

// Set data attributes
el.dataset.score = '100'; // adds data-score="100" to the HTML

// Remove a data attribute
delete el.dataset.score;
Common Trap

dataset values are always strings. data-active="true" gives you the string "true", not the boolean true. You need to convert: el.dataset.active === 'true' or Number(el.dataset.count).

setAttribute and getAttribute

For attributes that aren't covered by specific properties, use the generic attribute methods:

const link = document.querySelector('a');

link.getAttribute('href');          // read
link.setAttribute('href', '/new');  // write
link.removeAttribute('target');     // remove
link.hasAttribute('rel');           // check → true/false
Properties vs attributes

DOM elements have both properties (JavaScript object properties) and attributes (HTML markup attributes). They usually sync, but not always. For example, input.value (property) reflects the current value, while input.getAttribute('value') returns the initial value from the HTML. When in doubt, use properties for reading current state and setAttribute for setting HTML attributes.

Styling Elements

Inline Styles

const box = document.querySelector('.box');

// Set individual styles
box.style.backgroundColor = 'blue';
box.style.fontSize = '16px';
box.style.marginTop = '20px';

// Note: CSS properties use camelCase in JavaScript
// background-color → backgroundColor
// font-size → fontSize

Reading Computed Styles

element.style only reads inline styles. To get the actual computed value (including CSS from stylesheets), use getComputedStyle:

const el = document.querySelector('.box');

// Only reads inline styles
el.style.color; // "" (empty if set via stylesheet)

// Reads the actual computed value
getComputedStyle(el).color; // "rgb(0, 0, 0)"
getComputedStyle(el).fontSize; // "16px"
Key Rules
  1. 1Create elements with createElement, configure them in memory, then insert once
  2. 2Use DocumentFragment when inserting many elements to batch DOM updates
  3. 3textContent is safe for user input; innerHTML parses HTML and is an XSS risk with untrusted data
  4. 4classList.toggle is your best friend for toggling UI states
  5. 5dataset values are always strings — convert them manually for numbers and booleans
What developers doWhat they should do
Using innerHTML to insert user-generated text
innerHTML parses HTML, allowing script injection (XSS attacks). textContent treats input as plain text with no parsing
Using textContent for any user-supplied data
Expecting cloneNode to copy event listeners
cloneNode copies structure and attributes but not addEventListener handlers. If you need listeners on clones, add them after cloning
Re-attaching event listeners to cloned elements manually
Setting styles with style.background-color (kebab-case)
JavaScript property access doesn't support hyphens in property names. CSS property names must be converted to camelCase when used with element.style
Using camelCase: style.backgroundColor