JavaScriptSelecting Elements

Selecting Elements

Before you can change what's on the page, you have to find it. The DOM offers a small, well-defined set of methods for selecting elements — by id, by class, by tag, by name, or by any CSS selector you can write. Each returns a slightly different thing, and a couple of them behave in ways that surprise beginners. This page covers them all.

The minimal HTML we'll select from

index.html

HTML
<main id="app">
  <h1 class="title">Tasks</h1>
  <ul class="list">
    <li class="task">Buy milk</li>
    <li class="task done">Read DOM page</li>
    <li class="task">Write code</li>
  </ul>
  <input type="text" name="newTask" />
</main>
getElementById

The oldest and fastest selector. Returns a single element or null.

JS
const app = document.getElementById("app");
console.log(app);          // <main id="app">…</main>
console.log(app?.tagName); // "MAIN"

const missing = document.getElementById("nope");
console.log(missing);      // null

Ids are supposed to be unique. If two elements share an id (it happens), getElementById returns the first one in document order.

getElementsByClassName and getElementsByTagName

Return an HTMLCollection — a live, array-like list of matches. The collection updates automatically when the DOM changes.

JS
const tasks = document.getElementsByClassName("task");
console.log(tasks.length); // 3
console.log(tasks[0].textContent); // "Buy milk"

// Live behaviour: add a new .task and the collection grows.
const li = document.createElement("li");
li.className = "task";
li.textContent = "Sleep";
document.querySelector(".list").append(li);
console.log(tasks.length); // 4 — the same variable, now longer

getElementsByTagName("li") is the same idea but matches tag names instead of classes.

getElementsByName

Mostly useful for form fields, which often carry a name attribute. Returns a live NodeList.

JS
const inputs = document.getElementsByName("newTask");
console.log(inputs[0].value);
querySelector and querySelectorAll

The modern, expressive way to select. They accept any CSS selector — ids, classes, tags, combinators, attribute selectors, pseudo-classes.

JS
document.querySelector("#app");          // first match (or null)
document.querySelector(".task.done");    // a <li> that has both classes
document.querySelector("ul > li:first-child");
document.querySelector("input[name='newTask']");

const all = document.querySelectorAll(".task");
console.log(all.length);                 // 3
all.forEach((li, i) => console.log(i, li.textContent));
0 "Buy milk"
1 "Read DOM page"
2 "Write code"

querySelectorAll returns a NodeList. NodeLists have forEach, length, and indexing, but they are not arrays — no map, filter or reduce directly. Convert with Array.from(list) or [...list] when you need real array methods.

Live vs static collections

This is the subtle bit. Selectors come in two flavours:

  • LivegetElementsByClassName, getElementsByTagName, getElementsByName. The returned collection updates as the DOM changes.

  • StaticquerySelectorAll. The returned NodeList is a snapshot taken at the moment of the call. Later DOM changes do not affect it.

live vs static, side by side

JS
const liveTasks   = document.getElementsByClassName("task");
const staticTasks = document.querySelectorAll(".task");

console.log(liveTasks.length, staticTasks.length); // 3 3

const li = document.createElement("li");
li.className = "task";
li.textContent = "New one";
document.querySelector(".list").append(li);

console.log(liveTasks.length, staticTasks.length); // 4 3
Which should I use?
In modern code, default to `querySelector` / `querySelectorAll`. They cover every case the older APIs do and the static snapshot is usually what you actually want — iterating a live collection while you mutate it is a classic source of bugs.
Selecting within an element, not the whole document

Every element has its own querySelector and querySelectorAll. They search only inside that element's subtree, which keeps code modular and faster on big pages.

JS
const list = document.querySelector(".list");
const tasks = list.querySelectorAll(".task"); // only the <li>s inside this <ul>
Useful selector tricks
  • document.querySelectorAll("input, textarea, select") — match several tags at once.

  • document.querySelector("[data-id='42']") — find by data attribute.

  • document.querySelectorAll("li:not(.done)") — exclude with :not.

  • document.querySelectorAll(".task:nth-child(odd)") — every odd item.

  • document.documentElement.querySelector("…") — search from <html> if you ever need to.

When nothing matches

getElementById and querySelector return null. querySelectorAll returns an empty NodeList (length 0), never null. Always check before dereferencing:

JS
const banner = document.querySelector(".promo-banner");
if (banner) {
  banner.remove();
}
// Or with optional chaining:
document.querySelector(".promo-banner")?.remove();
Performance, in one line
For thousands of nodes, `getElementById` and `getElementsByClassName` are slightly faster than `querySelector`. For everything else — write the selector that reads clearly. Readability beats microseconds on a typical page.