The JavaScript DOM: Selecting, Changing and Creating Page Elements
What is the DOM in JavaScript?
The DOM (Document Object Model) is the browser's live, in-memory representation of a web page as a tree of nodes. Each HTML element, piece of text and comment becomes a node object that JavaScript can read and change. When your code modifies the DOM, for example by changing text or adding an element, the browser updates what visitors see on screen.
The DOM tree: nodes, elements and the document
When the browser parses HTML, it builds a tree. The document object sits at the top, <html> is its root element, and every nested tag becomes a child node. Text inside elements becomes text nodes. Relationships are described as parent, child and sibling, just like a family tree.
<ul id="menu">
<li class="active">Home</li>
<li>Courses</li>
</ul>
<!-- Tree:
document
└ html
└ body
└ ul#menu
├ li.active ─ "Home"
└ li ─ "Courses" -->The DOM is not the HTML file. It is a live object model built from it, so changes your script makes appear on screen but are never saved back to the file. Viewing the page source shows the original HTML; the Elements panel in dev tools shows the current DOM.
Selecting elements and reading or changing content
document.querySelector() takes any CSS selector and returns the first match, or null. querySelectorAll() returns a static NodeList of every match, which you can loop over with forEach. getElementById() is a quick lookup by id.
const menu = document.getElementById('menu');
const first = document.querySelector('#menu li');
const items = document.querySelectorAll('#menu li');
items.forEach(li => console.log(li.textContent));
first.textContent = 'Home page'; // plain text, safe
first.classList.add('highlight');
first.classList.toggle('active');
menu.setAttribute('aria-label', 'Main');textContent versus innerHTML
textContent reads and writes plain text; any angle brackets are shown literally. innerHTML parses its value as HTML. If that value includes anything a user typed or a server returned, an attacker can inject markup that runs script, an attack called cross-site scripting (XSS). Use textContent for data, and create elements with DOM methods when you need structure.
classList, attributes and dataset
Changing classes with classList.add(), remove() and toggle() keeps styling in CSS where it belongs. getAttribute() and setAttribute() work for any attribute. Custom data-* attributes appear on element.dataset, with hyphenated names converted to camelCase, so data-product-id becomes dataset.productId.
Creating, inserting and removing elements
const li = document.createElement('li');
li.textContent = 'Contact';
li.dataset.page = 'contact';
menu.append(li); // add at the end
menu.prepend(li.cloneNode(true));
li.remove(); // take it out again
// Batch inserts with a DocumentFragment
const cities = ['Pune', 'Jaipur', 'Kochi'];
const frag = document.createDocumentFragment();
for (const city of cities) {
const option = document.createElement('li');
option.textContent = city;
frag.append(option);
}
menu.append(frag); // one insertion into the live pageA DocumentFragment is a lightweight container that is not part of the page. Building many elements inside it and appending it once means the live document changes in a single step, which keeps your code tidy and avoids repeated layout work.
Events and event delegation
addEventListener(type, handler) runs a function when something happens, such as a click, input or submit. The handler receives an event object; event.target is the element that triggered it and event.preventDefault() stops default behaviour such as a form reloading the page.
Most events bubble up through the element's ancestors. Event delegation uses this: attach one listener to a parent and check event.target.closest() to find which child was clicked. It works for elements added later and avoids attaching hundreds of listeners.
Loading scripts with defer
Script that touches the DOM must run after the elements exist. Put your script in the <head> with the defer attribute: it downloads in parallel and runs, in order, once the HTML is parsed. Module scripts (type="module") are deferred automatically.
A working to-do list, built step by step
<!-- index.html -->
<form id="todo-form">
<label for="task">New task</label>
<input id="task" name="task" required>
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
<script src="todo.js" defer></script>// todo.js
const form = document.querySelector('#todo-form');
const input = document.querySelector('#task');
const list = document.querySelector('#todo-list');
form.addEventListener('submit', event => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
const li = document.createElement('li');
const label = document.createElement('span');
label.textContent = text; // never innerHTML for user input
const del = document.createElement('button');
del.type = 'button';
del.textContent = 'Delete';
del.dataset.action = 'delete';
li.append(label, ' ', del);
list.append(li);
form.reset();
input.focus();
});
// One delegated listener handles every current and future item
list.addEventListener('click', event => {
const li = event.target.closest('li');
if (!li) return;
if (event.target.dataset.action === 'delete') {
li.remove();
} else {
li.classList.toggle('done');
}
});- Write the HTML with a labelled input inside a form, so the Enter key and screen readers work without extra code.
- Load the script with
deferso the elements exist when it runs. - Select the form, input and list once and store them in constants.
- Handle
submit, prevent the page reload and ignore empty input. - Create the list item with
createElementandtextContent, then append it. - Use one delegated click listener on the list to delete items or toggle a
doneclass.
Good DOM habits
- Select elements once and reuse the reference instead of querying inside loops.
- Toggle classes and let CSS handle appearance, rather than setting many inline styles.
- Check for
nullbefore using the result ofquerySelector(). - Use real
<button>elements for clickable controls so they work with the keyboard. - Treat any data from users or API responses as untrusted text.
Practical exercise: extend the to-do list
Start from the to-do list above and the language basics in our JavaScript introduction.
- Show a count of remaining tasks that updates after every add, delete and toggle.
- Add a "Clear completed" button that removes every item with the
doneclass. - Save the list to
localStorageas JSON and rebuild it with aDocumentFragmentwhen the page loads. - Type
<img src=x onerror=alert(1)>as a task and confirm it appears as plain text.
You are done when the list survives a page refresh, the counter is always correct, keyboard users can add and delete tasks, and the test input shows as text rather than running. Projects like this connect to server-side PHP in SkillNewton's PHP foundation course.
Common mistakes
- Running DOM code from a script in the
<head>withoutdefer, soquerySelector()returnsnull. - Inserting user input with
innerHTML, which opens the page to cross-site scripting. - Attaching a separate listener to every list item instead of one delegated listener on the parent.
- Expecting
querySelectorAll()to return an array and calling array methods such asmapon it directly. - Using a clickable
<div>instead of a button, which keyboard users cannot reach.
Frequently asked questions
Is the DOM part of JavaScript?
No. The DOM is a web standard implemented by browsers, and JavaScript is the language used to work with it. Server-side JavaScript such as Node.js has no DOM unless a library provides one.
What is the difference between a NodeList and an array?
A NodeList supports forEach and indexing but lacks most array methods. Convert it with Array.from() or the spread syntax when you need map or filter.
When should I use innerHTML at all?
Only for markup you wrote yourself and fully control, such as a fixed template string with no user data in it. If any part comes from a visitor, a URL or a server response, build the elements with createElement and textContent instead.
Do I need a framework to manipulate the DOM?
No. Plain DOM methods handle forms, menus, tabs and lists well. Frameworks help with large applications, but learning the DOM first makes them much easier to understand.