Skip to main content
Adapted, not copied

Original notes for Javascript360.org β€” topic order inspired by Eloquent Javascript (4th ed.) by Marijn Haverbeke. See the full attribution note.

Chapter 14: The Document Object Model

Estimated time: ~40 minutes Β· Chapter type: Concept

What you'll learn​

  • How a page's HTML becomes a tree of Javascript objects
  • Finding, creating, and removing elements
  • The difference between attributes and properties
  • Basic layout and styling from code

Document structure​

When a browser loads HTML, it doesn't keep the raw text around β€” it parses it into a tree of objects called the Document Object Model (DOM), and that tree is what Javascript actually interacts with. Given this HTML:

<body>
<h1>Task List</h1>
<ul id="tasks">
<li>Write notes</li>
<li>Review code</li>
</ul>
</body>

...the resulting tree looks roughly like:

Every HTML tag becomes an element node; the text inside becomes a text node, a child of that element. This tree is exactly what you're navigating and modifying whenever you write DOM code.

Trees, briefly​

A tree is a data structure where each node has exactly one parent (except a single root) and any number of children β€” the same shape you'd use to represent a file system's folders, or an org chart. The DOM is one big tree rooted at document, and most DOM APIs are really just tree-traversal or tree-editing operations dressed up in web-specific names.

Moving through the tree​

Every node exposes relationships to its neighbors:

const list = document.getElementById('tasks');
list.parentNode; // the <body> element
list.children; // an HTMLCollection of the two <li> elements
list.firstElementChild; // the first <li>
list.children[0].nextElementSibling; // the second <li>

Finding elements​

getElementById is fast and specific; querySelector/querySelectorAll accept full CSS selectors and are what you'll reach for most often in modern code:

document.getElementById('tasks');            // one element, by id
document.querySelector('.task.done'); // first match for a CSS selector
document.querySelectorAll('li'); // a NodeList of every <li> on the page

querySelectorAll returns a static snapshot (a NodeList), which supports forEach directly:

document.querySelectorAll('li').forEach((item) => console.log(item.textContent));

Changing the document​

textContent reads or writes an element's text (safely β€” it never interprets its input as HTML); innerHTML reads or writes raw HTML, which is powerful but risky if the content ever comes from an untrusted source (a classic cross-site-scripting, or XSS, vulnerability):

const heading = document.querySelector('h1');
heading.textContent = 'Updated Task List'; // safe: always treated as plain text
// heading.innerHTML = userInput; // dangerous if userInput isn't sanitized β€” avoid

Creating and removing nodes​

const newItem = document.createElement('li');
newItem.textContent = 'Ship the release';

const list = document.getElementById('tasks');
list.appendChild(newItem); // add to the end
list.insertBefore(newItem, list.firstElementChild); // add to the start

newItem.remove(); // remove it again

A common pattern for adding several children at once is to build them off-screen first, in a DocumentFragment, and append the whole batch in one operation β€” this avoids forcing the browser to re-render the page after every single insertion:

const fragment = document.createDocumentFragment();
['Write tests', 'Deploy', 'Celebrate'].forEach((text) => {
const li = document.createElement('li');
li.textContent = text;
fragment.appendChild(li);
});
list.appendChild(fragment); // one DOM update instead of three

Attributes vs. properties​

This trips up a lot of people: an HTML attribute (what's written in the markup, always a string) and the corresponding Javascript property (a live value on the element object) are related but not always identical.

const checkbox = document.querySelector('input[type=checkbox]');
checkbox.getAttribute('checked'); // reflects the initial HTML β€” doesn't update as the user clicks
checkbox.checked; // the live, current state β€” this is what you want to read

For most standard properties (value, checked, id, className) the property is what you should use in code; getAttribute/setAttribute matter most for custom, non-standard attributes (data-* attributes especially) that don't have a dedicated property:

const card = document.querySelector('.card');
card.dataset.userId; // reads "data-user-id" β€” the dataset API camelCases attribute names automatically

classList is the standard way to add, remove, or toggle CSS classes without manually string-splitting className:

const item = document.querySelector('li');
item.classList.add('done');
item.classList.remove('pending');
item.classList.toggle('highlighted');
item.classList.contains('done'); // true

Layout, briefly​

Once the DOM tree exists, the browser computes where each element actually appears on screen β€” its layout. You can read the results of that computation from Javascript:

const box = document.querySelector('.card');
box.offsetWidth; // rendered width in pixels, including padding and border
box.getBoundingClientRect(); // { top, left, width, height, ... } relative to the viewport

Reading layout properties like these forces the browser to finish any pending layout calculation immediately (sometimes called a "forced reflow") β€” doing this repeatedly in a tight loop is a well-known performance trap, so batch your reads separately from your writes when working with many elements at once.

Styling​

You can set inline styles directly, though in most real applications you'll toggle CSS classes (as above) and let a stylesheet own the actual visual rules β€” it keeps presentation logic out of your Javascript:

const banner = document.querySelector('.banner');
banner.style.backgroundColor = 'darkred';
banner.style.display = 'none'; // a common way to hide an element

Key takeaways​

  • The DOM is a tree of node objects the browser builds from your HTML; Javascript reads and edits that tree, and the browser re-renders in response.
  • querySelector/querySelectorAll (CSS-selector based) are the modern, flexible way to find elements.
  • Use textContent for plain text (safe by default) and be cautious with innerHTML when the content isn't fully trusted.
  • Prefer live DOM properties (.checked, .value) over getAttribute for standard, interactive attributes; use dataset/classList for custom data and CSS classes.

Try it yourself​

  1. Given a <ul id="tasks"> with several <li> items, write code that adds a done class to every item whose text includes the word "Review."
  2. Write a function clearList(id) that removes every child from the element with the given id, without rebuilding the whole element from an HTML string.
  3. Explain why setting .innerHTML to a string containing a value typed by another user is risky, and what you'd use instead.
Hints
  1. document.querySelectorAll('#tasks li').forEach(li => { if (li.textContent.includes('Review')) li.classList.add('done'); });
  2. while (el.firstChild) el.removeChild(el.firstChild); β€” removing repeatedly from the front avoids re-indexing issues.
  3. Untrusted text could contain a <script> tag or an event-handler attribute, letting an attacker run arbitrary Javascript in your page β€” use textContent instead, which never parses its input as markup.

Summary​

The DOM is the bridge between your Javascript and everything the user actually sees β€” every dynamic web page you've ever used is, underneath, a script reading and rewriting this same tree structure. The next natural step is making that tree respond to what the user does, which is exactly what events are for.

Β 

🌟 Join the Javascript360 community

Connect with fellow JS developers, and get free interview prep, React challenges, and DSA guides.

Join on WhatsApp β†’