Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. JavaScript
  3. JavaScript DOM Manipulation Practice
JavaScript47 exercises

JavaScript DOM Manipulation Practice

Practice vanilla JS DOM manipulation: querySelector, createElement, event delegation with closest(), classList, textContent vs innerHTML (XSS prevention), and layout thrashing fixes.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Select the element with id "main-title".

On this page
  1. 1The DOM is a tree of nodes
  2. 2Selecting elements: querySelector is your default
  3. 3Traversing: moving through the tree
  4. 4Creating and inserting elements
  5. 5Updating content: textContent vs innerHTML
  6. 6classList: the right way to manage classes
  7. 7Event delegation: one listener for many elements
  8. 8Forms: preventDefault is essential
  9. 9Attributes vs properties
  10. 10Reading styles and geometry
  11. Computed styles vs inline styles
  12. Element position and size
  13. 11Performance: avoiding layout thrashing
  14. 12Quick reference
  15. 13References
The DOM is a tree of nodesSelecting elements: querySelector is your defaultTraversing: moving through the treeCreating and inserting elementsUpdating content: textContent vs innerHTMLclassList: the right way to manage classesEvent delegation: one listener for many elementsForms: preventDefault is essentialAttributes vs propertiesReading styles and geometryPerformance: avoiding layout thrashingQuick referenceReferences

In JavaScript, frameworks handle the DOM for you until they don't. Debugging "why didn't this click handler fire?", integrating a third-party widget, writing a browser extension, or patching a legacy page often means working directly with the DOM.

This page is a practical drill + reference for the DOM APIs you'll actually use: selecting, creating, inserting, updating, and handling events safely and efficiently.

Related JavaScript Topics
JavaScript FunctionsJavaScript CollectionsJavaScript Async/AwaitJavaScript Error Handling

The DOM is the browser's live tree of your HTML. JavaScript reads and modifies nodes; the browser re-renders when the tree changes.

The DOM (Document Object Model) is the browser's live representation of your HTML. Every element is a node in a tree structure. JavaScript can read and modify this tree, and the browser re-renders when it changes.

DOM operations are expensive compared to plain JS. Minimize direct DOM access by batching reads and writes.


Ready to practice?

Start practicing JavaScript DOM Manipulation with spaced repetition

Use querySelector/querySelectorAll with CSS selectors for all selection. Always guard against null—the element might not exist yet.

Modern DOM selection uses CSS selectors:

// Single element (first match)
const btn = document.querySelector(".submit-btn");

// Multiple elements (static NodeList)
const items = document.querySelectorAll(".list-item");

// Always guard against null
const panel = document.querySelector("#panel");
if (!panel) return;

Why querySelector over getElementById? For a printable reference of all DOM selection and manipulation methods, see the JavaScript DOM manipulation cheat sheet.

  • Consistent API (always CSS selectors)
  • Works on any element, not just document
  • More expressive: #id, .class, [data-attr], :not()

Watch out: If your script runs before the DOM is ready, elements won't exist yet. Either use defer on your script tag or wait for DOMContentLoaded.


Use closest() to walk up the tree (essential for event delegation), children for direct child elements, and matches() to test selectors without querying.

Once you have an element, you often need to find related elements:

// Parents and children
element.parentElement      // direct parent
element.children           // child elements (HTMLCollection)
element.firstElementChild  // first child element
element.nextElementSibling // next sibling element

// closest() - walk UP the tree (essential for delegation)
const card = button.closest(".card");  // finds nearest ancestor matching selector

// matches() - test if element matches selector
if (element.matches(".active")) { ... }

The closest() pattern is critical for event delegation. It finds the ancestor you care about regardless of which child was actually clicked.


Modern insertion methods are cleaner than the old appendChild:

// Create
const li = document.createElement("li");
li.textContent = "New item";
li.classList.add("list-item");
li.dataset.id = "123";

// Insert (relative to target element)
parent.append(li);        // as last child
parent.prepend(li);       // as first child
sibling.before(li);       // before sibling
sibling.after(li);        // after sibling

// Remove
li.remove();              // removes from DOM

For trusted HTML strings, use insertAdjacentHTML:

// Safer than el.innerHTML += "..."
el.insertAdjacentHTML("beforeend", "<li>New item</li>");

Positions: "beforebegin", "afterbegin", "beforeend", "afterend"


This is a security-critical distinction:

// SAFE: treats input as plain text (escapes HTML automatically)
element.textContent = userInput;

// RISKY: parses input as HTML
// Only use with trusted content, never with user input
element.innerHTML = trustedHtmlString;

Rule: Use textContent for user-provided content. For HTML, either sanitize first with a library like DOMPurify, or construct elements with createElement and textContent.

Side effect of innerHTML: Setting it destroys all child nodes (and their event listeners) and creates new ones. This is why event delegation matters.


The classList API is cleaner than manipulating className strings:

element.classList.add("active", "visible");    // add one or more
element.classList.remove("hidden");            // remove
element.classList.toggle("expanded");          // add if missing, remove if present
element.classList.toggle("dark", isDark);      // force on/off based on boolean
element.classList.contains("active");          // check
element.classList.replace("old", "new");       // swap

Why it matters: No more string splitting and rejoining. No accidental partial matches. Clean, readable code.


Event delegation: a click on a nested span bubbles up through the li to the ul where the listener is attached, closest finds the li

Instead of attaching listeners to every item:

// BAD: One listener per item (scales poorly, breaks on dynamic items)
items.forEach(item => item.addEventListener("click", handler));

// GOOD: One listener on parent, handle all children
list.addEventListener("click", (e) => {
  const target = e.target as Element;  // TypeScript: cast EventTarget
  const item = target.closest("li[data-id]");
  if (!item) return; // clicked something else

  // item is the <li> we care about
  handleItemClick(item.dataset.id);
});

Why delegation works:

  1. Events bubble up from target to ancestors
  2. closest() finds the right element regardless of click position
  3. Works for dynamically added elements automatically
  4. One listener uses less memory than hundreds

The pattern: Attach to stable parent -> use closest() to find target -> guard with if (!item) return. This listener-on-parent pattern is an application of the Observer design pattern, where one handler reacts to events from many sources.


Form submissions and link clicks trigger default browser behavior. Use preventDefault() to handle them in JavaScript:

form.addEventListener("submit", (e) => {
  e.preventDefault();  // stop page reload
  const data = new FormData(form);
  submitToAPI(data);
});

link.addEventListener("click", (e) => {
  e.preventDefault();  // stop navigation
  handleNavigation(link.href);
});

Also useful: stopPropagation() prevents the event from bubbling to parent handlers.


This distinction trips up even experienced developers:

// Attributes (HTML)
el.getAttribute("value");     // initial HTML value
el.setAttribute("data-id", "123");
el.dataset.id = "123";        // shorthand for data-* attributes

// Properties (live JS)
input.value;                  // current value (what user typed)
input.checked;                // current checked state

The gotcha: For form inputs, the attribute is the initial value; the property is the current state. They can diverge after user interaction.


Two common needs: getting CSS styles and getting element dimensions.

Computed styles vs inline styles

// element.style only reads INLINE styles (set via style attribute or JS)
el.style.color;  // "" if color comes from CSS

// getComputedStyle reads the ACTUAL computed value
const styles = getComputedStyle(el);
styles.color;  // "rgb(0, 0, 0)" - the real value

Element position and size

// getBoundingClientRect: position relative to viewport
const rect = el.getBoundingClientRect();
rect.top;     // distance from viewport top
rect.left;    // distance from viewport left
rect.width;   // element width (including padding, border)
rect.height;  // element height

// Dimensions only
el.offsetWidth;   // width including padding + border
el.offsetHeight;  // height including padding + border
el.clientWidth;   // width including padding (no border)

// Scroll position
el.scrollTop;     // pixels scrolled from top
el.scrollLeft;    // pixels scrolled from left

Layout thrashing: interleaving reads and writes forces N reflows, while batching all reads then all writes triggers only 1 reflow

Layout thrashing happens when you interleave reads and writes:

// BAD: Forces layout recalculation on each iteration
items.forEach(item => {
  const height = item.offsetHeight;  // READ (triggers layout)
  item.style.width = height + "px";  // WRITE (invalidates layout)
});

// GOOD: Batch reads, then batch writes
const heights = items.map(item => item.offsetHeight);  // all READs
items.forEach((item, i) => {
  item.style.width = heights[i] + "px";  // all WRITEs
});

For async DOM updates such as fetching data before inserting elements, see the async/await page.

For bulk insertions, use DocumentFragment:

const fragment = document.createDocumentFragment();
data.forEach(text => {
  const li = document.createElement("li");
  li.textContent = text;
  fragment.append(li);  // no reflow yet
});
ul.append(fragment);    // single reflow for all items

TaskMethod
Select onequerySelector(selector)
Select allquerySelectorAll(selector)
Find ancestorelement.closest(selector)
Test matchelement.matches(selector)
Createdocument.createElement(tag)
Insertappend, prepend, before, after
Removeelement.remove()
Safe textelement.textContent = text
ClassesclassList.add/remove/toggle
Data attributeselement.dataset.key
Stop defaulte.preventDefault()
Get positionel.getBoundingClientRect()
Read CSSgetComputedStyle(el).property
Event delegationparent.addEventListener + closest()

  • MDN: Document Object Model
  • MDN: Event delegation
  • javascript.info: Event delegation
  • web.dev: Avoid layout thrashing
  • OWASP: DOM XSS Prevention

When to Use JavaScript DOM Manipulation

  • Quick prototypes or widgets without a framework.
  • Debugging UI bugs where framework abstractions leak.
  • Browser extensions, injected scripts, or embedded widgets.
  • Legacy codebases where you can't adopt a framework.
  • Understanding what React/Vue do under the hood.

Check Your Understanding: JavaScript DOM Manipulation

Prompt

Add a click handler to a list using event delegation.

What a strong answer looks like

Attach one listener to the parent, use event bubbling, and locate the intended item with event.target.closest("li"). This avoids per-item listeners and handles dynamically added items.

What You'll Practice: JavaScript DOM Manipulation

querySelector / querySelectorAll (and selector strategy)Traversing: parentElement, children, closest(), matches()createElement, append, prepend, before, after, remove()textContent vs innerHTML (safe updates)Attributes: getAttribute / setAttribute + datasetclassList: add / remove / toggle / containsEvents: addEventListener, preventDefault(), stopPropagation()Event delegation (one listener for many items)Geometry: getBoundingClientRect(), offsetWidth/Height, scrollTopStyles: getComputedStyle() vs element.styleinsertAdjacentHTML / insertAdjacentElementDocumentFragment for batch insertions

Common JavaScript DOM Manipulation Pitfalls

  • querySelector returns null if not found (or DOM not ready)
  • innerHTML can introduce XSS if you inject untrusted strings
  • Replacing innerHTML removes child nodes and their listeners
  • Confusing event.target with event.currentTarget
  • Layout thrashing: mixing DOM reads and writes in loops
  • Forgetting that NodeList isn't an array (no map/filter directly)
  • Using element.style to read CSS (only reads inline styles)
  • Forgetting preventDefault() on form submit or link clicks

JavaScript DOM Manipulation FAQ

Why does querySelector sometimes return null?

Either the selector doesn't match anything, or the element isn't in the DOM yet. If your script runs before the DOM is parsed, wait for DOMContentLoaded or load scripts with the defer attribute.

textContent or innerHTML?

Use textContent for untrusted content (user input) to prevent XSS attacks. Only use innerHTML with trusted or properly sanitized HTML.

Why doesn't my delegated click handler work?

Make sure the event type bubbles (click does). Use event.target.closest(".selector") instead of assuming event.target is the button itself. Clicks on child elements bubble up.

What's the difference between attributes and properties?

Attributes are in HTML markup (getAttribute/setAttribute). Properties are live JS values on the element object (element.value). For inputs, the property reflects current state while the attribute reflects initial HTML.

What's the difference between event.target and event.currentTarget?

event.target is the element that triggered the event (clicked element). event.currentTarget is the element the listener is attached to. With delegation, target is the child; currentTarget is the parent.

How do I avoid layout thrashing?

Batch your reads together, then batch your writes. Reading offsetHeight then writing style, then reading again forces the browser to recalculate layout each time. Use requestAnimationFrame or DocumentFragment for batch updates.

NodeList vs HTMLCollection: what's the difference?

querySelectorAll returns a static NodeList (snapshot). getElementsByClassName returns a live HTMLCollection (updates as DOM changes). Both are array-like but not arrays. Use Array.from() or spread to convert.

Why did replacing innerHTML remove my event listeners?

Setting innerHTML destroys all child nodes and creates new ones. Event listeners attached to the old nodes are gone. Use event delegation on a parent that survives, or use textContent/insertAdjacentHTML for safer updates.

Why does element.style not show my CSS styles?

element.style only reads inline styles set via the style attribute or JavaScript. To read computed styles from CSS, use window.getComputedStyle(element).propertyName.

How do I get an element's position and size?

Use getBoundingClientRect() for position relative to viewport (top, left, width, height). For dimensions only, offsetWidth/offsetHeight include borders and padding. For scroll position, use scrollTop/scrollLeft.

What is DOM manipulation in JavaScript?

DOM manipulation means using JavaScript to read or change HTML elements on a page. Common operations include selecting elements (`querySelector`), changing content (`textContent`, `innerHTML`), modifying styles or classes (`classList`), and listening for events (`addEventListener`). The DOM is a tree structure that the browser builds from your HTML.

JavaScript DOM Manipulation Syntax Quick Reference

Select + guard
const btn = document.querySelector(".submit-btn");
if (!btn) return; // selector didn't match
Create + insert
const li = document.createElement("li");
li.textContent = "New item";
document.querySelector("ul")?.append(li);
Update classes
const el = document.querySelector("#panel");
el?.classList.toggle("is-open");
Safe text update
const out = document.querySelector("#output");
const userInput = "<script>alert(1)</script>";
if (out) out.textContent = userInput; // safer than innerHTML
Event delegation
const list = document.querySelector(".items");
list?.addEventListener("click", (e) => {
  const target = e.target as Element;
  const item = target.closest("li[data-id]");
  if (!item) return;
  console.log(item.dataset.id);
});
Form submit
form.addEventListener("submit", (e) => {
  e.preventDefault(); // stop page reload
  const data = new FormData(form);
  submitForm(data);
});
Traverse up (in handler)
button.addEventListener("click", (e) => {
  const card = (e.target as Element).closest(".card");
  const isActive = card?.matches(".active");
});
Batch insert with fragment
const frag = document.createDocumentFragment();
items.forEach(text => {
  const li = document.createElement("li");
  li.textContent = text;
  frag.append(li);
});
ul.append(frag); // single reflow
Get position and size
const rect = el.getBoundingClientRect();
console.log(rect.top, rect.left, rect.width, rect.height);
Read computed styles
const styles = getComputedStyle(el);
const color = styles.color; // works for CSS styles
// el.style.color only reads inline styles

JavaScript DOM Manipulation Sample Exercises

Example 1Difficulty: 1/5

Select the first element with class "card".

document.querySelector(".card")
Example 2Difficulty: 1/5

Select the first `<button>` element.

document.querySelector("button")
Example 3Difficulty: 2/5

Select ALL elements with class "item".

document.querySelectorAll(".item")

+ 44 more exercises

Quick Reference
JavaScript DOM Manipulation Cheat Sheet →

Copy-ready syntax examples for quick lookup

Related Design Patterns

Observer Pattern

Start practicing JavaScript DOM Manipulation

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.