⚡

JavaScript DOM Basics: Select, Change, and Listen to Elements

JavaScript3 min readUpdated October 2026

Learn how to find elements, change text and classes, create new nodes, and handle clicks with event delegation, plus how to avoid common XSS mistakes.

When a web page responds to you, whether it's opening a menu, adding a task, or showing an error, JavaScript is changing the DOM. The DOM is the browser's live version of your HTML, organized as a tree of elements you can read and modify.

The examples below are meant for the HTML editor. Paste one in, press Run, and click around.

Step 1: Find an element

Everything starts with selecting something. Two methods cover almost every case:

const title = document.querySelector("h1");          // first match
const items = document.querySelectorAll(".item");    // all matches
const box = document.getElementById("box");          // by id

querySelector takes any CSS selector, so you already know how to use it. If nothing matches it returns null, so check before using the result. querySelectorAll returns a list you can loop over with forEach or for...of.

Step 2: Change what's on the page

Here's a complete page you can run:

<h1 id="title">Hello</h1>
<button id="go">Change it</button>

<script>
  const title = document.querySelector("#title");
  const button = document.querySelector("#go");

  button.addEventListener("click", () => {
    title.textContent = "Hello, DOM!";
    title.style.color = "seagreen";
  });
</script>

textContent sets plain text. style changes inline styles, but there's a tidier way for anything beyond a one-off.

Use classes, not inline styles

Keep your appearance in CSS and let JavaScript just switch classes on and off.

<style>
  .card { padding: 12px; border: 1px solid #ccc; border-radius: 8px; }
  .card.active { background: #e8f7ee; border-color: seagreen; }
</style>

<div class="card" id="card">Click me</div>

<script>
  const card = document.querySelector("#card");
  card.addEventListener("click", () => {
    card.classList.toggle("active");
  });
</script>

classList has add, remove, toggle, and contains. It's cleaner than editing a long className string, and it keeps design decisions out of your script.

Create and add new elements

<ul id="list"></ul>
<button id="add">Add item</button>

<script>
  const list = document.querySelector("#list");
  let count = 0;

  document.querySelector("#add").addEventListener("click", () => {
    count++;
    const li = document.createElement("li");
    li.textContent = "Item " + count;
    list.append(li);
  });
</script>

The pattern is always the same: create the element, set its content, then attach it somewhere. append adds to the end, prepend to the start, and remove() deletes an element.

textContent vs innerHTML: a security issue

innerHTML lets you insert HTML from a string, which looks convenient. It's dangerous when the string includes anything a user typed.

<script>
  const name = '<img src=x onerror="alert(1)">';
  output.innerHTML = name;      // runs the attacker's code
  output.textContent = name;    // safely shown as plain text
</script>

If you display user input, use textContent. Only use innerHTML for markup you wrote yourself. This single habit prevents a whole class of cross-site scripting (XSS) bugs.

Event delegation: one listener for many items

Imagine a to-do list where items are added later. If you attach a click handler to each item, new ones won't have it. Instead, listen once on the parent and check what was clicked.

<ul id="todos">
  <li>Buy milk</li>
  <li>Write code</li>
</ul>
<button id="more">Add task</button>

<script>
  const todos = document.querySelector("#todos");

  todos.addEventListener("click", (event) => {
    const item = event.target.closest("li");
    if (item) item.classList.toggle("done");
  });

  document.querySelector("#more").addEventListener("click", () => {
    const li = document.createElement("li");
    li.textContent = "New task";
    todos.append(li);
  });
</script>

<style>
  .done { text-decoration: line-through; color: gray; }
  li { cursor: pointer; }
</style>

Click the old items and the new ones. They all work, because the event bubbles up from the clicked <li> to the <ul> where the single listener is waiting. It's also more efficient, since you have one handler instead of hundreds.

Reading values from inputs

<input id="name" placeholder="Your name" />
<button id="greet">Greet</button>
<p id="out"></p>

<script>
  document.querySelector("#greet").addEventListener("click", () => {
    const value = document.querySelector("#name").value.trim();
    document.querySelector("#out").textContent =
      value ? "Hi, " + value + "!" : "Please type a name.";
  });
</script>

Input values always come back as strings, even for number fields. Convert with Number(value) when you need math.

Common mistakes

  • Running the script before the element exists. Put your <script> after the HTML it uses, or add the defer attribute to external scripts.
  • Forgetting null checks. A typo in a selector returns null, and then null.textContent throws an error.
  • Repeatedly reading and writing layout. Build elements first and add them in one go if you're inserting hundreds of them.
  • Using innerHTML += in a loop. It rebuilds everything inside the element each time and destroys event listeners.

Key takeaways

  • Select with querySelector, change with textContent and classList.
  • Create elements with createElement, then append them.
  • Use textContent for anything a user typed.
  • Use event delegation for lists that change over time.

Try it yourself

Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.

Open the HTML/CSS editor

Related Articles