The DOM: a page that can be changed

Why:A browser keeps the page as a tree, and JavaScript can search it, read it and change it. Afterwards you’ll find elements with selectors, create new ones from data, and understand why other people’s text must never go in through innerHTML.

25 minutes

Read by speech synthesis (Qwen3-TTS); the voice is not a person’s.

The DOM is the document object model: the tree of nodes a browser builds from HTML. JavaScript finds elements in it, changes them and creates new ones.

A page you can touch

In the lesson on rendering you saw that the browser reads HTML and builds a DOM (Document Object Model) from it: a tree in which every tag, every piece of text and every attribute becomes a node. The page on screen is drawn from this tree. What matters for us: the browser hands the tree to JavaScript. In a script it’s available under the name document, and whatever the script changes in it, the browser redraws at once.

Every kind of interactivity works this way: a menu opens because a script removed the hidden attribute from an element; a counter grows because a script wrote a new number into an element; a list appears because a script created an element for each item and put it on the page. Three actions: find, read or change, create and insert.

Finding an element

The language has two commands for searching, and they take the same selectors you wrote in CSS: h3, .card, #offer, nav a.

const first = document.querySelector('.card');          // the first match, or null
const all = document.querySelectorAll('#offer .card');  // every match

querySelector returns one element, or null if nothing was found. querySelectorAll returns a list of nodes (a NodeList), possibly empty. You can walk it with forEach or for...of, and to use map and filter you turn it into an array: Array.from(all).

From a found element you can search further: card.querySelector('h3') looks only inside that card. That lets you write shorter selectors, and the script comes out tidier.

Try selectors on a small page, and what you find is highlighted in both the markup and the tree:

Find elements with a selector

Nothing found. querySelector would return null, and querySelectorAll an empty list.

Check yourself: what does querySelector return if there’s no such element?

null. So when an element may be missing, check the result, or the next line, such as el.textContent = ..., will fail with an error.

Reading and changing

Once an element is found, it has properties and methods:

  • textContent reads and writes the element’s text: title.textContent = 'New title';
  • getAttribute('href') and setAttribute('href', '#top') work with attributes. Many attributes also have short properties: link.href, input.value, el.id, el.hidden;
  • classList.add('selected'), classList.remove('selected') and classList.toggle('selected') manage classes. This is the best way to change how something looks: the script attaches a class, and CSS knows what to do with it. The styling stays in CSS and the logic in JS;
  • dataset gives access to attributes like data-price="1200": card.dataset.price. That’s how markup passes its own data to a script.

Creating and inserting

To add something new, you first create an element in memory, set it up, and only then put it in the tree:

const row = document.createElement('li');   // not on the page yet
row.textContent = 'A new item';
list.append(row);                           // now it is

Other commands you’ll need: prepend (to the start), remove() (take an element away) and replaceChildren(...) (replace all the contents). If you insert many elements in a row, build them in memory and insert them with a single append with several arguments: the browser then doesn’t have to redraw the page after each one.

Why other people’s text must not go in through innerHTML

There’s a shorter way too: the innerHTML property. You write a string of markup into it, and the browser parses it:

box.innerHTML = '<p>Hello, ' + name + '</p>';

It looks convenient, but if name came from a visitor (from a form field, a comment, a server’s answer, the page’s address), it may not be a name at all, but markup with an event handler. The browser sees no difference between “your” markup and “someone else’s”: it parses everything and runs the stranger’s code in the name of your site. This vulnerability is called XSS (cross-site scripting). It’s how sessions are stolen and pages are replaced.

The rule is simple: other people’s text goes in through textContent. It writes the string as text, the characters < and > stay ordinary letters, and nothing runs. Keep innerHTML for markup that you wrote in full yourself.

Try the same string both ways:

One string, two ways to insert it
box.textContent = string
text “<img src=x onerror=alert(1)>Anna”

Safe. One string of text; on the page it appears as plain letters.

box.innerHTML = string
 

No tags here. But it is not the string that protects you, it is the habit: other people’s text goes through textContent.

None of this is run: the string is parsed in an isolated document that is never inserted anywhere.

When a script runs

A mistake everyone trips over: a script looks for an element, and it isn’t there yet. An ordinary <script> stops the parsing of the page and runs at once, so everything lower in the document is still invisible to it. There are three ways out: put the script at the very end of the page, give it the defer attribute, or make it a module, as in our editor (type="module"). A module runs after the page is parsed, and you can search for anything in it.

Practice

A 2014 site. You already have a list of cards, but on a long page they can’t all be seen on the first screen. Let a script build a quick jump from the cards: a link to every offer at the top of the section, plus a counter.

The script invents nothing, it reads what is in the markup: it finds the cards, takes the heading of each and makes a link to it. Add one more card to the markup, and a link appears by itself. And if there are no cards, the visitor sees a clear message rather than an empty space.

Notice what is missing from the task: the word innerHTML. A card’s heading can contain anything, so everything goes through textContent.

What was wrong with the old DOM

There was a time when the DOM differed from browser to browser: Internet Explorer had some methods, Netscape had others, and document.all and document.layers had to be guessed at. A standard appeared in the late 1990s, and the browsers were slow to follow it. That confusion is why the jQuery library was born in 2006: one function, $('.card'), that worked the same in every browser. Millions of sites were built on it, and many still run on it, so if you read someone else’s code and see a dollar sign, you’re looking at jQuery.

Today browsers behave alike, the standard is called the DOM Standard (WHATWG), and querySelector, classList and append do the jobs for which a library used to be added. New sites don’t need jQuery: it weighs tens of kilobytes and solves problems that no longer exist. What stayed right is something else: find elements with selectors, change classes rather than styles, and never trust other people’s text.

Practice

Preparing your page…

Test

Question 1 of 7

What does document.querySelector return when you give it a selector that no element on the page matches?

One answer

Cards

These cards go into your review once you finish the lesson.

  • What is the DOM?

    Answer

    The tree of objects a browser builds from HTML. JavaScript gets it as document and can search, read and change the nodes. The page on screen is redrawn to follow.

  • How does querySelector differ from querySelectorAll?

    Answer

    querySelector finds the first matching element, or null. querySelectorAll returns a list of all the matches (possibly empty). The selectors are the same as in CSS.

  • How do you add a new element to a page?

    Answer

    Create it with createElement, set it up (textContent, attributes, classList) and put it in the tree with append. Until it is inserted, it isn’t on the page.

  • Why must other people’s text not go in through innerHTML?

    Answer

    innerHTML parses a string as markup. If it holds a tag with a handler, a stranger’s code runs on your page (XSS). textContent shows text as text.

  • What does querySelectorAll return, and what do you do with it?

    Answer

    A NodeList: a list you can walk with forEach or for...of. To use map and filter, turn it into an array first: Array.from(list).

Sources

Videos

JavaScript DOM Manipulation: full course for beginners (freeCodeCamp.org)

The video loads from youtube-nocookie.com only when you press play.

Open on the site

Next

You now know: the DOM.