JavaScriptCreating & Removing Elements

Creating Elements

Sometimes the page already has the structure you need and you only change text. More often, you build new DOM yourself — list items, cards, table rows, toast notifications. The classic recipe is create → configure → insert. This page shows that pattern, the modern insertion methods, and a few performance tricks that matter once you start building hundreds of nodes at a time.

createElement: making a node

document.createElement(tagName) returns a brand-new element that is not yet attached to the page. You configure it, then insert it.

JS
const li = document.createElement("li");
li.className = "task";
li.textContent = "New task";

document.querySelector(".list").append(li);

Until you insert it, the element exists only in memory. That is useful: you can build a whole subtree, fully configured, before touching the live page.

Setting properties and attributes

Most things can be set as a JavaScript property:

JS
const a = document.createElement("a");
a.href = "/about";
a.textContent = "About us";
a.className = "nav-link active";
a.id = "about-link";

Some attributes have no matching property — data-*, custom ARIA attributes, or attributes for components that don't ship with the browser. Use setAttribute then:

JS
a.setAttribute("data-track", "nav-about");
a.setAttribute("aria-current", "page");

We'll cover the attribute vs property distinction in detail on the next page; for now, "use the property if there is one, fall back to setAttribute" is a good rule.

Inserting: append, prepend, before, after

The modern insertion API is small and consistent:

  • parent.append(...nodes) — add as last children.

  • parent.prepend(...nodes) — add as first children.

  • sibling.before(...nodes) — insert just before this element.

  • sibling.after(...nodes) — insert just after this element.

  • old.replaceWith(...nodes) — swap old out for the new nodes.

four positions, no math

JS
const list = document.querySelector(".list");
const newItem = document.createElement("li");
newItem.textContent = "Fresh";

list.append(newItem);   // last child
list.prepend(newItem);  // first child (the same node is moved, not copied)

const middle = list.children[1];
middle.before(newItem); // just before middle
middle.after(newItem);  // just after middle
Move, don't copy
Each of those calls *moves* the same `newItem` to a new location. A DOM node can only be in one place at a time. To duplicate, use `newItem.cloneNode(true)` (the `true` means "deep — copy descendants too").
Strings and elements together

append, prepend, before and after accept any mix of strings and nodes. Strings are inserted as text nodes — safe, no HTML parsing.

JS
const p = document.createElement("p");
const strong = document.createElement("strong");
strong.textContent = "Heads up:";
p.append(strong, " backups complete.");
// <p><strong>Heads up:</strong> backups complete.</p>
replaceWith and remove

swap and delete

JS
const banner = document.querySelector(".old-banner");
const fresh = document.createElement("div");
fresh.className = "new-banner";
fresh.textContent = "Welcome back!";
banner.replaceWith(fresh);

document.querySelector(".tooltip")?.remove();

element.remove() detaches the element from its parent. It does not destroy it — you can keep the reference and re-insert it later. Letting the reference go out of scope is what frees the memory.

Building bigger subtrees

a card from scratch

JS
function buildCard({ title, body, href }) {
  const card = document.createElement("article");
  card.className = "card";

  const h = document.createElement("h2");
  h.textContent = title;

  const p = document.createElement("p");
  p.textContent = body;

  const link = document.createElement("a");
  link.href = href;
  link.textContent = "Read more";

  card.append(h, p, link);
  return card;
}

document.body.append(
  buildCard({ title: "DOM", body: "Trees of nodes.", href: "/dom" }),
  buildCard({ title: "Events", body: "Bubbling and capture.", href: "/events" }),
);
DocumentFragment: many nodes, one insertion

Inserting nodes one by one into a live page triggers layout each time. When you have many to add, build them into a DocumentFragment first and insert the fragment in one shot — only the final insertion causes a layout pass.

JS
const list = document.querySelector(".list");
const frag = document.createDocumentFragment();

for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = "Task " + i;
  frag.append(li);
}

list.append(frag); // one reflow instead of 1000
1000 <li> elements appear, with one layout pass instead of a thousand.
Cloning

JS
const template = document.querySelector(".task-template");
const copy = template.cloneNode(true); // deep clone
copy.classList.remove("task-template");
copy.querySelector(".title").textContent = "New task";
document.querySelector(".list").append(copy);

For more elaborate templates, the <template> element lets you write inert HTML in your markup and clone its contents with template.content.cloneNode(true) — no parsing of user strings needed.

Workflow that scales
Build in memory, insert in one call. Use `textContent` for user data. Reach for `<template>` and `DocumentFragment` once you have more than a handful of nodes. Everything else falls into place from there.