Learn by reading through in order

Creating, Changing, and Removing Elements — createElement / classList / remove

Covers createElement and append for adding elements, remove for taking them out, classList and dataset for changing classes and attributes, and textContent for showing input as plain text.

If you assign to innerHTML for every change, such as adding an item to a list, marking one as done, or deleting one, you rebuild even the li elements you meant to keep. And if you assign user input the same way, any tags mixed into it, such as <a>, become elements too.

This article covers how to create and remove elements, classList and dataset for changing classes and attributes, and how to insert input as plain text.

Creating and Adding Elements — createElement and append

Say you want to add "Send the invoice" to the end of a task list. If you tack a string onto innerHTML and assign it back, every li already in the list is rebuilt as a new element too. An li you stored in a variable earlier with querySelector still points to the old element, which is no longer on the page, so changing its text through that variable doesn't change what's displayed.

createElement (a method that takes a tag name and creates a new element that isn't anywhere in the DOM tree yet) is called like document.createElement("li"). The element you create becomes part of the page once you add it to a parent element with append (a method that adds the element you pass to the end of the calling element's children).

document.body.innerHTML = `<ul id="tasks"><li>Prepare the quote</li><li>Share the meeting notes</li></ul>`;
const taskList = document.querySelector("#tasks");

// Create an li element and put text in it (it isn't on the page yet)
const newTask = document.createElement("li");
newTask.textContent = "Send the invoice";
console.log(newTask.outerHTML);                        // <li>Send the invoice</li>
console.log(document.querySelectorAll("li").length);   // 2

// Adding it to the end of the ul's children makes it part of the page
taskList.append(newTask);
console.log(document.querySelectorAll("li").length);   // 3
console.log(taskList.outerHTML);
// <ul id="tasks"><li>Prepare the quote</li><li>Share the meeting notes</li><li>Send the invoice</li></ul>
How a New li Gets onto the Page
createElement("li")newTask.textContent ="Send the invoice"taskList.append(newTask)Creates <li></li>;2 li on the page<li>Send theinvoice</li>still off the pageGoes at the end ofthe ul; now 3 li
After the createElement and textContent lines, there are still only 2 li elements. The count goes up on the line that calls append.

The variable newTask points to the same li element before and after append. So if you assign to newTask.textContent after calling append, the text of the third li on the page changes.

Add newly received inquiries to a list of pending inquiries all at once. The list's ul element, inquiryList, and the array of inquiry subjects, newInquiries, are already declared.

① Turn each subject in the array into an li element and add it to the end of the list.

② Print how many li elements are on the page.

③ Print the list's HTML, including the ul element's own tags.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Marking Tasks as Done — classList and dataset

Next, you want to add a done class to a finished task so that CSS shows it with a strikethrough. The li's class attribute is a single string, task urgent, so if you overwrite it with done, you also lose the task class that CSS uses to style the row.

classList (an object for working with an element's classes one word at a time) has add to add a word, remove to take one out, and toggle to add it if it's missing or remove it if it's there. Attributes other than class are written with setAttribute(name, value), and attributes that start with data- are written through dataset (an object for reading and writing data attributes by name).

document.body.innerHTML = `<ul><li class="task urgent" data-id="T-1">Prepare the quote</li><li class="task urgent" data-id="T-2">Send the invoice</li></ul>`;
const quote = document.querySelector('[data-id="T-1"]');
const invoice = document.querySelector('[data-id="T-2"]');

// classList adds and removes one word at a time (the other classes stay)
quote.classList.add("done");
console.log(quote.outerHTML);    // <li class="task urgent done" data-id="T-1">Prepare the quote</li>
quote.classList.toggle("urgent");   // It's done, so drop urgent (it's present, so toggle removes it)
console.log(quote.outerHTML);    // <li class="task done" data-id="T-1">Prepare the quote</li>

// setAttribute replaces the whole value (using it on class wipes out task too)
invoice.setAttribute("class", "done");
console.log(invoice.outerHTML);  // <li class="done" data-id="T-2">Send the invoice</li>

// Write attributes that start with data- through dataset (doneAt becomes data-done-at)
invoice.dataset.doneAt = "9/10";
console.log(invoice.outerHTML);
// <li class="done" data-id="T-2" data-done-at="9/10">Send the invoice</li>
add Adds, setAttribute Replaces
quote.classList.add("done")Adds the word doneto task urgentclass istask urgent doneThe task rowkeeps its styleinvoice.setAttribute("class", "done")Replaces the wholeclass valueclass isjust doneThe task rowloses its style
add keeps the 2 words and adds another, while setAttribute replaces the value with done. To add a class, use classList.

The name you use with dataset is the attribute name with data- removed and the letter after each remaining hyphen capitalized. You read and write data-id as dataset.id and data-done-at as dataset.doneAt, and assigning to them writes the attribute into the HTML under its original name.

dataset Doesn't Accept Hyphenated Names

If you read with the hyphen left in, as in dataset["done-at"], you get undefined instead of an error, so the typo is easy to miss. Assigning that way throws an error on that line. The error is named SyntaxError, but unlike a real syntax error in your code, it happens at run time, so the lines before it still run.

Update the done classes on a new-hire onboarding checklist. The list's HTML is already set up.

① Add the done class to item C-2.

② Item C-1 was marked done by mistake. Toggle its done class with a single call.

③ Write a completion-date data attribute on item C-2.

④ Print the list's HTML, including the ul element's own tags.

JavaScript / TypeScript Editor

Run code to see output

Removing Elements — remove and References Left in Variables

Now suppose you want to take a finished task out of the list. If you rewrite the list with innerHTML, you also have to write the classes and data attributes added in the previous section back into the HTML string, and if you miss even one, a task you kept loses its done marker.

remove (a method that detaches the calling element from its parent and removes it from the DOM tree) is called with no arguments on the element you want to delete, as in doneItem.remove(). The classList.remove("done") from the previous section only takes a word out of the class. It's a different method from this one, which removes the element itself.

document.body.innerHTML = `<ul id="tasks"><li data-id="T-1">Prepare the quote</li><li data-id="T-2" class="done">Send the invoice</li><li data-id="T-3">Share the meeting notes</li></ul>`;
const tasks = document.querySelectorAll("li");

// Find the finished li and remove it from the DOM tree
const doneItem = document.querySelector(".done");
doneItem.remove();
console.log(document.querySelectorAll("li").length);   // 2

// The removed element and the NodeList you got earlier both stay in their variables
console.log(doneItem.textContent);                     // Send the invoice
console.log(tasks.length);                             // 3 (the elements found at that time)

// It's still in the variable, so append can put it back at the end of the list
document.querySelector("#tasks").append(doneItem);
console.log(document.querySelectorAll("li").length);   // 3
A Fresh Count vs. a Stored Count
querySelectorAll("li") againSearches the DOMafter removeThe T-1 andT-3 lilength is 2tasks, takenbefore removeSame elements aswhen it was takenThe T-1, T-2,and T-3 lilength is 3
Querying again after remove finds only T-1 and T-3, while tasks still has 3. To check the count, query again.

querySelectorAll just returns the elements it found at the moment you called it; it doesn't track later changes to the DOM. The removed doneItem also stays in its variable, so you can still read its text or put it back into the list with append.

On an email compose screen, remove all the canceled attachments from the list at once. The list's HTML and its ul element, attachmentList, are already set up.

① Get all the canceled file elements and remove them from the list one at a time.

② After removing them, print how many elements ① got.

③ Get the canceled file elements again and print how many there are.

④ Print the list's HTML, including the ul element's own tags.

JavaScript / TypeScript Editor

Run code to see output

Showing Input as Text — textContent and innerHTML

Consider a product review section that shows comments posted by users. Comments are saved, and the same string appears on other visitors' pages too. If a comment contains an a tag (a tag that creates a link, with the destination in its href attribute), assigning it to innerHTML turns the fake link the poster planted into a real link on the page.

A string assigned to innerHTML is parsed as HTML, and its tags become elements. A string assigned to textContent isn't parsed, so even < goes in as a plain character. Putting input into innerHTML doesn't just let people plant fake links; it can also lead to XSS (a vulnerability where code a user slips into their input runs on other visitors' pages).

document.body.innerHTML = `<p class="review"></p>`;
const review = document.querySelector(".review");
// A string a user typed into the review field (with a fake link mixed in)
const reviewText = '<a href="https://example.com/login">Log in again</a> to continue';

// Put into innerHTML, the a tag becomes a real a element
review.innerHTML = reviewText;
console.log(document.querySelectorAll("a").length);   // 1

// Put into textContent, the tags go in as plain text
review.textContent = reviewText;
console.log(document.querySelectorAll("a").length);   // 0
console.log(review.outerHTML);
// <p class="review">&lt;a href="https://example.com/login"&gt;Log in again&lt;/a&gt; to continue</p>
The p After the Same Input Goes In
p after assigning to innerHTML
a element — href is example.com/login
  • The text inside is "Log in again"
  • Clicking it goes to another site
Text — to continue
  • Follows the a element
p after assigning to textContent
Text — the whole input as one piece
  • <a href="…">Log in again</a> to continue
  • 0 a elements
With innerHTML, the a tag becomes an element; with textContent, it stays as text. textContent is what keeps input from becoming elements.

The &lt; and &gt; in outerHTML are how HTML writes < and > as text. When you read them back with textContent, you get the original characters. The table below sorts the ways of changing elements covered in this article by what they change.

What you changeHow to write itWhat to watch for
The element itselfcreateElement / append / removeNot on the page until append
classclassList.add / remove / togglesetAttribute replaces the whole value
Attributes starting with data-dataset.doneAt = "9/10"Drop data-, capitalize after hyphens
Other attributessetAttribute("href", "/tasks/T-2")Adds it, or replaces its value
Text contenttextContentPut user input here
HTML contentinnerHTMLOnly HTML you wrote yourself

Code in an img's onerror Still Runs

A <script> put into innerHTML doesn't run, but if an img tag, which displays an image, is written as <img src="x" onerror="…">, onerror (an attribute holding code that runs when loading fails) does run. So the fact that <script> doesn't run doesn't make it safe to put input into innerHTML.

Show one post in a product review section. The poster's name author, the comment comment, and the section element reviewArea are already declared.

① Create an article element, then add a strong element holding the poster's name and a p element holding the comment inside it, in that order. Insert the input as plain text.

② Add the article to the section and print how many a elements are on the page.

③ Print the HTML, including the article element's own tags.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1Right after const li = document.createElement("li");, where on the page is li?

Q2If an li's class is task urgent, what is its class after setAttribute("class", "done")?

Q3After p.textContent = "<b>New</b>";, how many b elements are inside p?