What Is the DOM?
When a browser loads an HTML page, it converts every tag into a JavaScript object. These objects form a tree structure called the Document Object Model (DOM).
Think of it like this:
- Your HTML file is a blueprint.
- The DOM is the actual building the browser constructs from that blueprint.
- JavaScript is the tool you use to remodel the building after it's built.
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>The browser turns this into a tree:
document
└── html
└── body
├── h1 → "Hello"
└── p → "World"
Every box in the tree is a node. Tags become element nodes, text inside them becomes text nodes.
Selecting Elements
Before you can change anything, you need to grab the element. The DOM gives you a few ways to do this.
getElementById
Finds one element by its id attribute.
<h1 id="title">Hello</h1>const title = document.getElementById("title");
console.log(title.textContent); // "Hello"querySelector
Finds the first element that matches a CSS selector. This is the most flexible option.
const title = document.querySelector("#title"); // by id
const firstParagraph = document.querySelector("p"); // by tag
const btn = document.querySelector(".my-button"); // by classquerySelectorAll
Returns all matching elements as a NodeList (similar to an array).
const allParagraphs = document.querySelectorAll("p");
allParagraphs.forEach((p) => {
console.log(p.textContent);
});Rule of thumb: Use querySelector / querySelectorAll for almost everything. They accept any CSS selector, so they're the most versatile.
Changing Content
textContent
Gets or sets the plain text inside an element.
const title = document.querySelector("#title");
title.textContent = "New title!";innerHTML
Gets or sets the HTML inside an element. Be careful — setting innerHTML with user-provided strings can introduce security vulnerabilities (XSS).
const container = document.querySelector("#container");
container.innerHTML = "<strong>Bold text</strong>";Tip: Prefer textContent when you're just dealing with plain text. Use innerHTML only when you genuinely need to insert HTML markup and you trust the source.
Changing Styles
Every element has a .style property that lets you set inline CSS.
const title = document.querySelector("#title");
title.style.color = "tomato";
title.style.fontSize = "2rem";Notice that CSS property names like font-size become camelCase (fontSize) in JavaScript.
Adding/Removing CSS Classes
A better approach is to toggle CSS classes instead of setting inline styles.
const box = document.querySelector(".box");
box.classList.add("active"); // adds the "active" class
box.classList.remove("active"); // removes it
box.classList.toggle("active"); // adds if missing, removes if present
box.classList.contains("active"); // returns true or falseCreating and Removing Elements
Creating a New Element
// 1. Create the element
const newParagraph = document.createElement("p");
// 2. Give it content
newParagraph.textContent = "I was created with JavaScript!";
// 3. Add it to the page
document.body.appendChild(newParagraph);Inserting Before Another Element
const list = document.querySelector("ul");
const newItem = document.createElement("li");
newItem.textContent = "Inserted item";
// Insert before the first child
list.insertBefore(newItem, list.firstChild);Removing an Element
const item = document.querySelector("#old-item");
item.remove();Handling Events
Events let you react to things the user does — clicks, typing, hovering, etc.
addEventListener
const button = document.querySelector("#my-button");
button.addEventListener("click", () => {
alert("Button was clicked!");
});Common Events
| Event | When It Fires |
|---|---|
click | User clicks the element |
input | User types in an input field |
submit | A form is submitted |
keydown | User presses a key |
mouseover | Mouse moves over the element |
The Event Object
Every event handler receives an event object with useful info.
const input = document.querySelector("#search");
input.addEventListener("input", (event) => {
console.log(event.target.value); // the current text in the input
});Preventing Default Behavior
Some elements have built-in behavior. For example, submitting a form reloads the page. You can stop that with preventDefault().
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stops the page from reloading
console.log("Form submitted without reload!");
});Attributes
You can read and write HTML attributes with JavaScript.
const link = document.querySelector("a");
// Read
console.log(link.getAttribute("href"));
// Write
link.setAttribute("href", "https://example.com");
// Remove
link.removeAttribute("target");For common attributes like id, className, src, and value, you can also access them directly:
const img = document.querySelector("img");
img.src = "new-image.png";
img.alt = "A new image";Traversing the DOM
Sometimes you need to move between related elements.
const item = document.querySelector("#current");
item.parentElement; // the parent element
item.children; // all child elements
item.firstElementChild; // first child element
item.lastElementChild; // last child element
item.nextElementSibling; // the next sibling element
item.previousElementSibling; // the previous sibling elementBuilding a Simple Todo App
Now let's put everything together. Here's a complete, working todo app using only HTML, CSS, and vanilla JavaScript.
What It Does
- Type a task and press Enter or click "Add"
- Each task appears in a list
- Click a task to mark it as done (strikethrough)
- Click the "x" button to delete a task
The Code
Create a single file called todo.html and paste this in:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Todo App</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #1a1a2e;
color: #eee;
display: flex;
justify-content: center;
padding-top: 60px;
min-height: 100vh;
}
.app {
width: 100%;
max-width: 420px;
padding: 0 16px;
}
h1 {
font-size: 1.8rem;
margin-bottom: 24px;
color: #e2b714;
}
.input-row {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.input-row input {
flex: 1;
padding: 10px 14px;
border: 1px solid #333;
border-radius: 8px;
background: #16213e;
color: #eee;
font-size: 1rem;
outline: none;
}
.input-row input:focus {
border-color: #e2b714;
}
.input-row button {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: #e2b714;
color: #1a1a2e;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
}
.input-row button:hover {
background: #f0c830;
}
ul {
list-style: none;
}
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
margin-bottom: 8px;
background: #16213e;
border-radius: 8px;
border: 1px solid #333;
cursor: pointer;
transition: opacity 0.2s;
}
li.done .task-text {
text-decoration: line-through;
opacity: 0.5;
}
li .delete-btn {
background: none;
border: none;
color: #e74c3c;
font-size: 1.2rem;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
li .delete-btn:hover {
color: #ff6b6b;
}
.empty {
text-align: center;
color: #666;
margin-top: 32px;
}
</style>
</head>
<body>
<div class="app">
<h1>Todo App</h1>
<form class="input-row" id="todo-form">
<input type="text" id="todo-input" placeholder="What needs to be done?" />
<button type="submit">Add</button>
</form>
<ul id="todo-list"></ul>
<p class="empty" id="empty-message">No tasks yet. Add one above!</p>
</div>
<script>
// 1. Select the elements we need
const form = document.querySelector("#todo-form");
const input = document.querySelector("#todo-input");
const list = document.querySelector("#todo-list");
const emptyMessage = document.querySelector("#empty-message");
// 2. Keep track of todos in an array
let todos = [];
// 3. Render the todo list to the DOM
function render() {
// Clear the current list
list.innerHTML = "";
// Show or hide the empty message
emptyMessage.style.display = todos.length === 0 ? "block" : "none";
// Create an <li> for each todo
todos.forEach((todo, index) => {
// Create the <li> element
const li = document.createElement("li");
if (todo.done) {
li.classList.add("done");
}
// Create the text span
const textSpan = document.createElement("span");
textSpan.classList.add("task-text");
textSpan.textContent = todo.text;
// Clicking the text toggles done
textSpan.addEventListener("click", () => {
todos[index].done = !todos[index].done;
render();
});
// Create the delete button
const deleteBtn = document.createElement("button");
deleteBtn.classList.add("delete-btn");
deleteBtn.textContent = "✕";
deleteBtn.addEventListener("click", (event) => {
event.stopPropagation(); // don't trigger the li click
todos.splice(index, 1);
render();
});
// Assemble the <li>
li.appendChild(textSpan);
li.appendChild(deleteBtn);
list.appendChild(li);
});
}
// 4. Handle form submission
form.addEventListener("submit", (event) => {
event.preventDefault(); // prevent page reload
const text = input.value.trim();
if (text === "") return; // ignore empty input
// Add the new todo to our array
todos.push({ text: text, done: false });
// Clear the input field
input.value = "";
// Re-render the list
render();
});
// 5. Initial render
render();
</script>
</body>
</html>How It Works — Step by Step
-
Select elements — We grab the form, input, list, and empty message using
querySelector. -
Store data in an array — The
todosarray holds objects like{ text: "Buy groceries", done: false }. This is our "source of truth". -
Render function — Every time something changes, we call
render(). It clears the list, loops through thetodosarray, and creates fresh<li>elements for each one. This pattern of "data changes → re-render the UI" is the same idea behind frameworks like React. -
Handle form submit — We listen for the
submitevent, prevent the default page reload, grab the input value, push it into our array, and re-render. -
Toggle done — Clicking the task text flips
todo.doneand re-renders. The CSS class.doneapplies the strikethrough style. -
Delete — Clicking the "x" button removes the todo from the array with
splice()and re-renders. We useevent.stopPropagation()so the click doesn't also toggle done.
Try It
Save the code as todo.html and open it in your browser. That's it — no build tools, no frameworks, no npm install. Just a file and a browser.
Summary
Here's a cheat sheet of everything we covered:
| Task | Method |
|---|---|
| Select one element | document.querySelector(selector) |
| Select all matching | document.querySelectorAll(selector) |
| Change text | element.textContent = "new text" |
| Change HTML | element.innerHTML = "<b>bold</b>" |
| Change style | element.style.color = "red" |
| Add/remove class | element.classList.add("name") |
| Create element | document.createElement("tag") |
| Add to page | parent.appendChild(child) |
| Remove from page | element.remove() |
| Listen for events | element.addEventListener("click", fn) |
| Prevent default | event.preventDefault() |
| Read attribute | element.getAttribute("href") |
| Set attribute | element.setAttribute("href", "url") |
The DOM API is the foundation of every web application. Even if you use React or Vue, understanding the DOM helps you debug faster and write better code.
You might also like
Build Connect Four from Scratch with Vanilla JS
A complete guide to building Connect Four using only HTML, CSS, and vanilla JavaScript. Covers gravity-based piece dropping, win detection across rows/columns/diagonals, and AI opponent. Interview-ready implementation.
FrontendBuild a Hierarchical Checkbox from Scratch with Vanilla JS
A complete guide to building a hierarchical (tri-state) checkbox tree using only HTML, CSS, and vanilla JavaScript. Covers parent-child propagation, indeterminate state, dynamic trees, and accessibility. Interview-ready implementation.
FrontendJavaScript Promises — From Zero to Interview Ready
A beginner-friendly deep dive into Promises. Understand the concept, master the API, and learn to implement custom Promise utilities that interviewers love to ask.