HTML builds the structure. CSS makes it look right. But neither of them responds when a user clicks a button, fills out a form, or types into a search box.
That's JavaScript's job — specifically, JavaScript's ability to read and rewrite the DOM. Understanding DOM manipulation is the moment web development stops being static and starts feeling alive.
What Is the DOM?
When a browser loads an HTML page, it doesn't just display it — it builds a representation of the entire page in memory. This representation is called the Document Object Model, or DOM.
The DOM is a tree. At the top is document — the entire page. Below that, every HTML element becomes a node: the <body> contains a <main>, which contains an <article>, which contains an <h1>, and so on, all the way down to the individual text inside each element.
JavaScript can access this tree, walk through it, read it, and modify it. When JavaScript changes a node, the browser immediately updates what's visible on screen — no page reload required.
Step 1: Selecting Elements
Before you can change anything, you need to find it. JavaScript gives you several ways to select elements from the DOM.
querySelector — Your Primary Tool
// Select by CSS selector — returns the first match, or null
const heading = document.querySelector('h1');
const button = document.querySelector('.submit-btn');
const panel = document.querySelector('#user-panel');querySelector accepts any valid CSS selector — tag names, classes, IDs, attribute selectors, combinators. It returns the first element that matches, or null if nothing matches.
querySelectorAll — When You Need Multiple Elements
// Returns a NodeList of all matching elements
const allCards = document.querySelectorAll('.card');
allCards.forEach(card => {
console.log(card.textContent);
});Always Check for null
const panel = document.querySelector('#user-panel');
// Without this check, your code crashes if the element doesn't exist
if (panel) {
panel.style.display = 'block';
}If you call a method on null, you get a TypeError that crashes your script. Check first.
Step 2: Reading and Writing Content
Once you have an element, you can read what's inside it and write new content.
textContent — Safe for Plain Text
const title = document.querySelector('h1');
// Read
console.log(title.textContent); // "Welcome to Devstiny"
// Write
title.textContent = 'Quest Complete';textContent gets or sets the plain text of an element and all its descendants. It's safe because it doesn't parse HTML — if you write <strong> into textContent, it shows up as literal characters, not bold text.
innerHTML — For When You Need HTML
const card = document.querySelector('.card');
card.innerHTML = '<h2>New Title</h2><p>New description.</p>';innerHTML parses and renders HTML, so you can insert tags. But there's a critical warning: never set innerHTML from user input. If a user types <script>alert("hacked")</script> and you put that directly into innerHTML, it executes. This is called an XSS vulnerability. Use textContent for user-generated content, always.
Step 3: Changing Classes and Styles
The cleanest way to change how an element looks is to add or remove CSS classes. Define the visual state in your CSS file, then toggle it with JavaScript.
// In your CSS
.hidden { display: none; }
.card--active { border-color: #4ECDC4; }
// In your JavaScript
const card = document.querySelector('.card');
const modal = document.querySelector('.modal');
card.classList.toggle('card--active'); // Toggle on/off
modal.classList.add('hidden'); // Add a class
modal.classList.remove('hidden'); // Remove a class
if (card.classList.contains('card--active')) {
console.log('Card is active');
}Prefer classList — it keeps your CSS in your CSS file. You can also set inline styles directly, but this is generally less clean and should be avoided for anything defined in a stylesheet.
Step 4: Creating and Removing Elements
JavaScript can build entirely new elements and add them to the page, or remove existing ones.
// Create a new element
const newCard = document.createElement('div');
newCard.className = 'card';
newCard.textContent = 'New item';
// Add it to the page
const container = document.querySelector('.container');
container.appendChild(newCard); // Adds at the end
container.prepend(newCard); // Adds at the beginning
// Remove an element
const oldItem = document.querySelector('.outdated-item');
oldItem.remove();This is the foundation of dynamic web apps — when you load more results without refreshing the page, when you add items to a cart, when a notification appears in the corner — this is what's happening.
Step 5: Responding to Events
The most important part of DOM manipulation is making things happen in response to what users do.
addEventListener
const button = document.querySelector('#submit-btn');
button.addEventListener('click', function(event) {
console.log('Button clicked');
event.preventDefault(); // Prevent default form submission
});The first argument is the event name. The second is a callback function that runs when the event fires.
Common events:
| Event | Triggers When |
|---|---|
| click | User clicks an element |
| submit | A form is submitted |
| input | User types in an input field |
| change | A select/checkbox/radio changes |
| keydown | A key is pressed |
| mouseenter / mouseleave | Mouse enters or leaves an element |
Event Delegation: One Listener for Many Elements
Adding individual listeners to 100 list items is inefficient. Event delegation attaches a single listener to a parent element and uses event.target to handle clicks on its children.
const list = document.querySelector('.items-list');
list.addEventListener('click', function(event) {
// Check if a list item was clicked
if (event.target.matches('.item')) {
event.target.classList.toggle('item--selected');
}
});
// Works even for elements added to the list AFTER the listener was attachedPutting It Together: A Complete Example
const input = document.querySelector('#task-input');
const addButton = document.querySelector('#add-btn');
const taskList = document.querySelector('#task-list');
// Add a task
addButton.addEventListener('click', function() {
const text = input.value.trim();
if (!text) return;
const li = document.createElement('li');
li.className = 'task-item';
li.textContent = text;
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.className = 'remove-btn';
li.appendChild(removeBtn);
taskList.appendChild(li);
input.value = '';
});
// Remove a task (event delegation)
taskList.addEventListener('click', function(event) {
if (event.target.matches('.remove-btn')) {
event.target.parentElement.remove();
}
});The Key Principles to Remember
- ▸Select before you modify — Always get a reference to the element first
- ▸Check for null — querySelector returns null if nothing matches; handle it
- ▸Use classList over inline styles — Keep visual logic in CSS
- ▸Prefer textContent for user content — innerHTML is powerful and dangerous
- ▸Event delegation scales better — One parent listener beats many child listeners
What Comes After DOM Manipulation
Once you're comfortable with the DOM, the next territory is making network requests — fetching data from APIs without reloading the page (the Fetch API and async/await). After that, you'll find that frameworks like React are largely abstractions over the same ideas: selecting elements, managing state, and responding to events.
The DOM is where JavaScript starts to feel like real power.
In Devstiny's Chapter 4, the DOM is taught through The Wired District — a city where every element exists but nothing renders. Somers, the realm's jester-logician, guides you through selection, manipulation, and events to restore the district node by node. Start learning at devstiny.com.
