How would you prepare DOM manipulation, event listeners, and styling with vanilla JS
Use modern DOM APIs (`querySelector`, `closest`, `dataset`, `classList`), prefer event delegation, batch DOM writes after reads to avoid layout thrash, scope styles via CSS classes/custom properties not inline, clean up listeners with AbortController on teardown. Keep DOM operations declarative-ish: template via `<template>` or DOMParser, swap entire subtrees instead of micromanaging.
Vanilla JS is the substrate beneath every framework. Knowing it well makes you a better React/Vue dev too.
Selecting elements
document.querySelector("#header"); // one
document.querySelectorAll(".card"); // NodeList (forEach, no map)
const items = [...document.querySelectorAll("li")]; // make it an Array
e.target.closest("[data-id]"); // walk up to matching ancestorDon't use getElementById etc. except in hot loops where the slightly faster path matters.
Creating elements
// Direct
const el = document.createElement("div");
el.className = "card";
el.textContent = "hello";
parent.append(el);
// From a template
const tpl = document.createElement("template");
tpl.innerHTML = `<article class="card"><h2></h2><p></p></article>`;
const node = tpl.content.firstElementChild.cloneNode(true);
node.querySelector("h2").textContent = data.title;
node.querySelector("p").textContent = data.body;
parent.append(node);<template> is the right shape for repeated render. innerHTML for simple chunks (mind XSS).
Reading / writing safely
textContentfor text (XSS-safe).innerHTMLfor HTML strings (sanitize first).setAttribute("data-id", id)orel.dataset.id = id.classList.add / remove / toggle / contains.
Event listeners
const ac = new AbortController();
button.addEventListener("click", onClick, { signal: ac.signal });
window.addEventListener("resize", onResize, { signal: ac.signal });
// On teardown:
ac.abort(); // removes all listeners attached with this signalAbortController is the cleanest way to clean up many listeners at once.
Delegation
list.addEventListener("click", (e) => {
const item = e.target.closest("[data-id]");
if (!item) return;
handle(item.dataset.id);
});One listener for N children; works for dynamically added rows.
Avoiding layout thrash
// BAD — read after write per item
items.forEach((el) => { el.style.width = el.offsetWidth + 10 + "px"; });
// GOOD — batch reads, then writes
const widths = items.map((el) => el.offsetWidth);
items.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });Mixing reads and writes in a loop forces sync layout per iteration.
Styling
- Toggle classes, don't set inline styles for state changes.
- Use CSS custom properties for dynamic values that need JS-driven updates:
el.style.setProperty("--x",${x}px). - Avoid
element.style.cssText = ...— clobbers existing styles.
Animation
requestAnimationFrame(() => {
el.style.transform = `translateY(${y}px)`;
});For smooth animations, use transform + opacity (composite-only), tie to rAF.
Forms
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
await save(data);
});FormData reads all named inputs; Object.fromEntries builds the object.
Mutation observation
new MutationObserver((mutations) => { ... }).observe(target, { childList: true, subtree: true });For watching DOM changes you didn't make. Don't poll.
IntersectionObserver / ResizeObserver
For lazy load, infinite scroll, container queries' polyfill — much better than scroll/resize event spam.
Common mistakes
- One listener per row (use delegation).
innerHTMLwith user input (XSS).- Inline style mutation for state (use classes).
- Long synchronous loops blocking paint.
- Reading layout inside a write loop.
Interview framing
"Modern selectors (querySelector, closest), event delegation with AbortController for cleanup, batch DOM reads/writes to avoid layout thrash, CSS classes + custom properties for styling (not inline style mutation for state), <template> and cloneNode for repeated render. FormData + Object.fromEntries for form data. Observers (Mutation, Intersection, Resize) instead of polling. Watch for XSS when using innerHTML — prefer textContent. Animate via transform + opacity inside rAF for 60fps."
Follow-up questions
- •How does AbortController help with listeners?
- •Why prefer classes over inline styles?
- •When would you use IntersectionObserver?
Common mistakes
- •innerHTML with user input.
- •Layout thrash from read-after-write.
- •Polling instead of observers.
- •Manual style mutations for state.
Performance considerations
- •Layout thrash, delegation, observers, rAF — all about keeping the main thread free.
Edge cases
- •Shadow DOM scoping.
- •Custom elements lifecycle.
- •Touch events vs pointer events.
Real-world examples
- •Stripe Elements, htmx, Web Components, classic jQuery patterns rewritten in vanilla.