Text
el.textContent = "Painted"; // safe: whatever the string is, it stays text
el.textContent; // reads all descendant text
el.innerHTML = "<b>Painted</b>"; // parses markup — only for markup YOU wrote
el.innerText = "…"; // as rendered; slower, forces layout
A name of <img src=x onerror=alert(1)> becomes a running script. textContent is immune, because it never parses.
Classes
Class is the boundary between JavaScript and CSS. Script decides the state; the stylesheet decides what that state looks like. A handler that sets el.style.background has taken a design decision into the wrong file.
el.classList.add("is-open");
el.classList.remove("is-open");
el.classList.toggle("is-open");
el.classList.toggle("is-open", shouldBeOpen); // force a state
el.classList.contains("is-open");
el.classList.replace("old", "new");
// the old way loses every other class
el.className = "is-open";
Attributes, properties and data
el.setAttribute("aria-expanded", "true");
el.getAttribute("href");
el.removeAttribute("disabled");
el.toggleAttribute("hidden", shouldHide);
el.disabled = true; // a property — the right way for booleans
el.value = "x"; // the live value; the attribute is the default
el.dataset.panel = "specs"; // data-panel="specs"
Number(el.dataset.openDelay); // always a string, so convert
Boolean attributes care only about presence. Use the property — el.disabled = false — or removeAttribute.
Styles
Set a custom property from script and let the stylesheet decide what it means. The script says --p: 0.62; the CSS says width: calc(var(--p) * 100%). Nothing about the look lives in the script.
el.style.transform = "translateX(20px)"; // a computed value
el.style.setProperty("--p", 0.62); // a custom property the CSS uses
getComputedStyle(el).color; // the final value, as a string
el.style.color; // only what is in the style attribute
Creating elements
const li = document.createElement("li");
li.className = "row";
li.textContent = door.name;
li.dataset.id = door.id;
list.append(li); // inside, at the end — takes several nodes and strings
list.prepend(li); // inside, at the start
li.before(other); // outside, before
li.after(other); // outside, after
li.replaceWith(other);
li.remove();
list.replaceChildren(); // empty it — better than innerHTML = ""
Insert once
A DocumentFragment is a node with no parent: appending to it costs nothing, and appending the fragment inserts all its children at once and leaves it empty.
// 500 separate insertions
for (const d of doors) list.append(row(d));
// one insertion
const frag = document.createDocumentFragment();
for (const d of doors) frag.append(row(d));
list.append(frag);
// or build the whole list and swap it in
list.replaceChildren(...doors.map(row));
Templates
This keeps the shape of a row in the HTML and only the values in the script — which is easier to read than a long template literal and impossible to make an injection hole.
const tpl = document.getElementById("row-tpl");
function row(door) {
const node = tpl.content.cloneNode(true);
node.querySelector(".title").textContent = door.name;
node.querySelector(".days").textContent = `${door.days} days`;
node.querySelector("a").href = `/doors/${door.id}/`;
return node;
}
Read then write
Reading a geometric property — offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle — makes the browser lay out immediately so it can answer. Alternating reads and writes makes it do that once per item.
// BAD: every read after a write forces a synchronous layout
for (const el of items) {
el.style.height = el.offsetHeight + 10 + "px";
}
// GOOD: read everything, then write everything
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + 10 + "px"; });