Selecting and updating the DOM
DOM and events15 min readLesson 1 of 4
1.1 The document tree
When the browser parses an HTML file it builds a tree of nodes. The document is the root, elements sit under it, and text inside an element is a child node. JavaScript receives an object model of that tree, and the DOM API reads or changes it.
Every change to the tree is visible on the next paint. The DOM is live: reading a property after an update returns the new value, and the browser recalculates layout and paint when the script yields control. That liveness is why DOM code feels immediate and why heavy loops over thousands of nodes feel slow.
The document object is the entry point. It exposes query methods, creation methods, and shortcuts such as document.body. The window object sits above the document and represents the browser window; for this module the document is the part that matters.
1.2 Selecting elements
document.querySelector returns the first element matching a CSS selector or null. document.querySelectorAll returns a static NodeList of every match. A NodeList is not an array, though it supports forEach and can be spread into one.
Select by id with #app, by class with .card, by attribute with [data-kind], or by tag with button. Prefer a specific selector over a broad one. A selector that matches one element today can match several after the next feature, and code written for the first match silently updates the wrong one.
Ids are the fastest and clearest hook when a page has a single landmark. Classes suit repeated components. Data attributes are useful when JavaScript needs a value that is not visual, such as data-task-id.
const app = document.createElement("div");
app.id = "app";
document.body.append(app);
app.innerHTML = `
<h1>Tasks</h1>
<ul><li class="task">Learn DOM</li><li class="task">Build a page</li></ul>
`;
const heading = document.querySelector("h1");
const tasks = document.querySelectorAll(".task");
console.log(heading.textContent);
console.log(tasks.length);Output
Tasks 2
1.3 Reading and updating text
textContent reads or writes the text inside an element. It treats the value as text, so angle brackets appear as characters rather than creating elements. That safety makes textContent the default choice for anything that comes from a user or an API.
innerHTML reads or writes HTML. It can create a whole subtree from a string, which is convenient for static templates but dangerous for user input. A string containing a script tag or an event attribute becomes live markup, an injection attack. Keep innerHTML for trusted template strings and use textContent for data.
Reading innerHTML serialises the current subtree, including attributes and whitespace. For a value the user typed, textContent on the specific element is smaller and more predictable.
const card = document.createElement("article");
card.className = "card";
card.innerHTML = "<h2>JavaScript</h2><p>DOM practice</p>";
document.body.append(card);
const title = card.querySelector("h2");
title.textContent = "Updated title";
card.classList.add("active");
card.dataset.kind = "lesson";
console.log(title.textContent);
console.log(card.classList.contains("active"));
console.log(card.dataset.kind);Output
Updated title true lesson
1.4 Classes, attributes and data
classList is the safe way to change classes. add, remove, toggle and contains read like the operations they perform, and toggle returns a boolean for the new state. Assigning className replaces every class and is easy to misuse when another script owns part of the list.
setAttribute and getAttribute handle attributes such as href, src and aria-label. For boolean attributes, presence matters more than the value. Setting disabled to any string disables the control; remove the attribute to enable it again.
dataset maps data attributes to properties. data-task-id="7" becomes element.dataset.taskId === "7". Values are always strings, so convert numbers before comparing them with numeric ids.
1.5 Creating and inserting nodes
document.createElement makes a detached element that is not yet in the page. Set its content and attributes, then insert it with append, prepend, before, after or replaceWith. append accepts several nodes and strings, while appendChild takes one node and is the older API.
Building a subtree detached and inserting it once is faster than inserting into the page on every step, because each insertion can trigger layout work. For a list, create a document fragment, append items to it, and append the fragment once. The browser then does one insertion instead of dozens.
const list = document.createElement("ul");
document.body.append(list);
["Plan", "Build", "Test"].forEach((label) => {
const item = document.createElement("li");
item.textContent = label;
list.append(item);
});
const first = list.querySelector("li");
first.remove();
console.log(list.children.length);
console.log(JSON.stringify([...list.children].map((item) => item.textContent)));Output
2 ["Build","Test"]
1.6 The window and the document
The window object represents the browser tab. It owns the document, the location, the history and the timers. window.document is the same object as the global document, and a property on window can be written without the prefix.
For page work, start from the document. Scroll position, viewport size and global events such as resize and scroll live on window, which is why a sticky header or a scroll listener attaches there. Mixing the two is fine; knowing which object owns an API saves a search.
A script can add and remove window listeners just like element listeners. A listener left on window outlives the component that added it, so cleanup is part of the feature rather than an optional refinement.
1.7 Performance of DOM updates
Every write to the DOM can invalidate layout and force the browser to recalculate positions. Reading a layout property such as offsetHeight right after a write flushes pending changes, a pattern known as layout thrashing. Batch reads, then batch writes, rather than alternating between them.
For a list, build the items in a DocumentFragment and append once. For text that changes often, update a single text node instead of rebuilding a subtree. For a page with many independent updates, requestAnimationFrame groups visual work into one frame.
Measure before optimising. A task list with a few dozen items rebuilds fast enough, and clarity matters more. The browser's performance panel shows whether layout, script or paint is the bottleneck before any rewrite.
1.8 Accessibility hooks
The DOM is the interface for assistive technology as well as for the eye. Use a real button for a clickable action, a real label for each form field, and a heading order that reflects the page structure. A div with a click listener looks clickable but is invisible to a keyboard user unless it also has a role, a tabindex and key handling.
When a script updates a message area, aria-live="polite" tells a screen reader to announce the change. When a control is disabled, use the disabled attribute rather than only a CSS class. These attributes are part of the same DOM API the rest of the module uses, and they cost almost nothing.
1.9 Common mistakes
- Using innerHTML with text from a user or an API.
- Selecting an element before the script runs, so querySelector returns null.
- Reading
.lengthon a NodeList returned by querySelector, which has no such property. - Replacing className and removing classes another part of the code needs.
- Comparing
dataset.idwith a number without converting the string. - Appending inside a loop and triggering a layout on every pass.
Exam
DOM questions describe a page and a script, then ask what changes. Sketch the tree, mark the element each selector reaches, then apply the writes in order. A null selector is the most common failure, so check the element exists before the code runs.
- Create an element, set its text and append it to the body.
- Select all items with a class and log the count.
- Toggle a class and read it back with contains.
- Remove one child and check the parent's child count.
