Selecting DOM Elements in JavaScript Full Guide

Learn every way to select DOM elements in JavaScript: getElementById, querySelector, querySelectorAll, getElementsByClassName, and more with practical examples.

JavaScriptbeginner
11 min read

Before you can change anything on a web page, you need to select the element you want to change. JavaScript provides multiple methods for finding elements in the DOM, each with different strengths. Choosing the right selector method affects both code readability and performance.

This guide covers every built-in DOM selection method, explains when to use each one, and provides the patterns that experienced developers rely on daily.

The Two Main Approaches

DOM selection methods fall into two categories:

  1. Modern query methods: querySelector and querySelectorAll use CSS selector syntax
  2. Classic getter methods: getElementById, getElementsByClassName, getElementsByTagName

Both work in all modern browsers. The modern methods are more flexible; the classic methods are slightly faster for simple lookups.

querySelector: Select the First Match

querySelector returns the first element that matches a CSS selector, or null if nothing matches:

javascriptjavascript
// By tag name
const heading = document.querySelector("h1");
 
// By class
const card = document.querySelector(".card");
 
// By id
const main = document.querySelector("#main");
 
// By attribute
const external = document.querySelector('a[target="_blank"]');
 
// Complex selectors
const firstNavLink = document.querySelector("nav > ul > li:first-child a");
const activeItem = document.querySelector(".sidebar .item.active");

querySelector accepts any valid CSS selector, which makes it the most versatile selection method:

javascriptjavascript
// Pseudo-selectors work too
const firstParagraph = document.querySelector("article p:first-of-type");
const lastItem = document.querySelector("ul li:last-child");
const checkedBox = document.querySelector('input[type="checkbox"]:checked');
 
// Combine multiple conditions
const importantNote = document.querySelector("div.note.important:not(.archived)");

Scoping querySelector to a Parent

You can call querySelector on any element, not just document. This limits the search to that element's descendants:

javascriptjavascript
const sidebar = document.querySelector(".sidebar");
 
// Only searches inside the sidebar
const sidebarLink = sidebar.querySelector("a");
const sidebarHeading = sidebar.querySelector("h2");

This is more efficient than searching the entire document and ensures you get the right element when multiple sections have similar content.

querySelectorAll: Select All Matches

querySelectorAll returns a NodeList of all elements matching the selector:

javascriptjavascript
// All paragraphs on the page
const paragraphs = document.querySelectorAll("p");
console.log(paragraphs.length); // Number of paragraphs
 
// All elements with the "card" class
const cards = document.querySelectorAll(".card");
 
// All checked checkboxes
const checked = document.querySelectorAll('input[type="checkbox"]:checked');
 
// Multiple selectors (comma-separated)
const headings = document.querySelectorAll("h1, h2, h3");

Iterating Over Results

The returned NodeList supports .forEach() directly:

javascriptjavascript
const buttons = document.querySelectorAll("button");
 
// forEach works directly
buttons.forEach(button => {
  button.addEventListener("click", handleClick);
});
 
// for...of loop also works
for (const button of buttons) {
  button.classList.add("styled");
}
 
// Convert to array for full array methods
const buttonArray = Array.from(buttons);
const primaryButtons = buttonArray.filter(btn => btn.classList.contains("primary"));

Important: The NodeList from querySelectorAll is static. It is a snapshot taken at the time of the query. New elements added to the page after the query are not included.

getElementById: Select by ID

getElementById is the fastest DOM selection method. It returns a single element by its id attribute, or null:

javascriptjavascript
const app = document.getElementById("app");
const sidebar = document.getElementById("sidebar");
const submitBtn = document.getElementById("submit-btn");
 
// No # prefix - just the ID value
// document.getElementById("#app"); // WRONG - returns null
// document.getElementById("app");  // CORRECT

Key detail: getElementById is only available on the document object, not on individual elements. IDs must be unique per page.

javascriptjavascript
// This does NOT work
const section = document.querySelector(".section");
// section.getElementById("item"); // TypeError
 
// Use querySelector on elements instead
const item = section.querySelector("#item");

getElementsByClassName: Select by Class

getElementsByClassName returns a live HTMLCollection of elements with the specified class:

javascriptjavascript
const cards = document.getElementsByClassName("card");
console.log(cards.length); // Number of elements with class "card"
 
// Access by index
const firstCard = cards[0];
const secondCard = cards[1];
 
// Multiple classes (space-separated = AND logic)
const activeCards = document.getElementsByClassName("card active");
// Returns only elements that have BOTH "card" AND "active" classes

Live Collection vs Static NodeList

getElementsByClassName returns a live collection. This means it automatically updates when the DOM changes:

javascriptjavascript
const items = document.getElementsByClassName("item");
console.log(items.length); // 3
 
// Add a new element with class "item"
const newItem = document.createElement("div");
newItem.className = "item";
document.body.appendChild(newItem);
 
console.log(items.length); // 4 - automatically updated!
 
// querySelectorAll returns a STATIC NodeList
const staticItems = document.querySelectorAll(".item");
document.body.appendChild(document.createElement("div"));
console.log(staticItems.length); // Still the original count

Warning: Live collections can cause bugs in loops that add or remove elements matching the selector:

javascriptjavascript
const items = document.getElementsByClassName("remove-me");
 
// BUG: as items are removed, the collection shrinks, skipping elements
for (let i = 0; i < items.length; i++) {
  items[i].remove(); // Collection shrinks, index shifts
}
 
// Fix 1: Loop backward
for (let i = items.length - 1; i >= 0; i--) {
  items[i].remove();
}
 
// Fix 2: Convert to static array first
Array.from(items).forEach(item => item.remove());
 
// Fix 3: Use querySelectorAll (static)
document.querySelectorAll(".remove-me").forEach(item => item.remove());

getElementsByTagName: Select by Tag

getElementsByTagName returns a live HTMLCollection of elements with the specified tag name:

javascriptjavascript
const paragraphs = document.getElementsByTagName("p");
const images = document.getElementsByTagName("img");
const allElements = document.getElementsByTagName("*"); // Everything
 
// Can be scoped to an element
const nav = document.querySelector("nav");
const navLinks = nav.getElementsByTagName("a");

This is a fast method for broad selections by tag, but querySelectorAll("p") is more common in modern code because it returns a static collection and uses the same syntax for all queries.

Method Comparison Table

MethodReturnsStatic/LiveCSS SelectorsSpeed
querySelectorSingle element or nullStaticFull CSS supportFast
querySelectorAllNodeListStaticFull CSS supportFast
getElementByIdSingle element or nullN/AID onlyFastest
getElementsByClassNameHTMLCollectionLiveClass onlyVery fast
getElementsByTagNameHTMLCollectionLiveTag onlyVery fast

When to Use Each Method

javascriptjavascript
// Use getElementById for single elements with an ID
const app = document.getElementById("app");
 
// Use querySelector for complex, one-time selections
const activeTab = document.querySelector(".tabs .tab.active");
const firstInput = document.querySelector('form input[required]:first-of-type');
 
// Use querySelectorAll for multiple element selections
const allCards = document.querySelectorAll(".card");
const formFields = document.querySelectorAll("input, select, textarea");
 
// Use getElementsByClassName when you need live updating
const notifications = document.getElementsByClassName("notification");
// notifications.length updates automatically as they're added/removed

Real-World Example: Dynamic Form Validator

Here is a practical example combining multiple selection methods:

javascriptjavascript
function setupFormValidation(formId) {
  const form = document.getElementById(formId);
  if (!form) return;
 
  // Select all required fields
  const requiredFields = form.querySelectorAll("[required]");
  const submitButton = form.querySelector('button[type="submit"]');
  const errorContainer = form.querySelector(".error-messages");
 
  function validateField(field) {
    const value = field.value.trim();
    const label = field.closest("label")?.textContent
      || field.getAttribute("placeholder")
      || field.name;
 
    if (!value) {
      field.classList.add("invalid");
      field.classList.remove("valid");
      return { field: label, message: `${label} is required` };
    }
 
    if (field.type === "email" && !value.includes("@")) {
      field.classList.add("invalid");
      field.classList.remove("valid");
      return { field: label, message: `${label} must be a valid email` };
    }
 
    field.classList.remove("invalid");
    field.classList.add("valid");
    return null;
  }
 
  function validateAll() {
    const errors = [];
    requiredFields.forEach(field => {
      const error = validateField(field);
      if (error) errors.push(error);
    });
 
    // Update error display
    errorContainer.innerHTML = errors.length > 0
      ? errors.map(e => `<p class="error">${e.message}</p>`).join("")
      : "";
 
    submitButton.disabled = errors.length > 0;
    return errors.length === 0;
  }
 
  // Validate on input
  requiredFields.forEach(field => {
    field.addEventListener("input", validateAll);
    field.addEventListener("blur", () => validateField(field));
  });
 
  // Final validation on submit
  form.addEventListener("submit", event => {
    if (!validateAll()) {
      event.preventDefault();
    }
  });
}
 
setupFormValidation("signup-form");

Performance Tips

Cache Your Selections

javascriptjavascript
// BAD: querying the DOM in every loop iteration
for (let i = 0; i < 100; i++) {
  document.querySelector(".counter").textContent = i; // 100 lookups!
}
 
// GOOD: cache the reference
const counter = document.querySelector(".counter");
for (let i = 0; i < 100; i++) {
  counter.textContent = i; // 1 lookup, 100 updates
}

Use Scoped Queries

javascriptjavascript
// SLOW: searches the entire document
const sidebarTitle = document.querySelector(".sidebar .section .title");
 
// FASTER: narrow the search scope
const sidebar = document.querySelector(".sidebar");
const section = sidebar.querySelector(".section");
const title = section.querySelector(".title");

Use getElementById for IDs

javascriptjavascript
// Slightly slower - parses CSS selector
const el = document.querySelector("#my-element");
 
// Slightly faster - direct hash table lookup
const el = document.getElementById("my-element");

The difference is small (microseconds), but if you are selecting by ID in a hot path, getElementById is the fastest option.

Common Mistakes to Avoid

Using # with getElementById

javascriptjavascript
// WRONG - getElementById doesn't use CSS syntax
const el = document.getElementById("#main"); // Returns null!
 
// CORRECT
const el = document.getElementById("main");

Treating NodeList Like an Array

javascriptjavascript
const items = document.querySelectorAll(".item");
 
// NodeList does NOT have .map, .filter, .reduce
// items.map(item => item.textContent); // TypeError
 
// Convert to array first
const texts = Array.from(items).map(item => item.textContent);
 
// Or use spread
const textsAlt = [...items].map(item => item.textContent);

Not Handling Null Returns

javascriptjavascript
// querySelector returns null if no match
const element = document.querySelector(".nonexistent");
element.textContent = "Hello"; // TypeError: Cannot set property of null
 
// Always check first
const element = document.querySelector(".nonexistent");
if (element) {
  element.textContent = "Hello";
}
 
// Or use optional chaining
document.querySelector(".nonexistent")?.classList.add("found");

Best Practices

  1. Use querySelector/querySelectorAll as your default. They handle any selector pattern with consistent, familiar CSS syntax.
  2. Use getElementById for ID-based lookups when performance matters. It is the fastest single-element selection method.
  3. Cache DOM references in variables. Never query the same element repeatedly, especially inside loops.
  4. Scope queries to parent elements when possible. parent.querySelector(".child") is faster and more precise than document.querySelector(".parent .child").
  5. Convert NodeLists to arrays with Array.from() or spread ([...nodeList]) when you need .map(), .filter(), or .reduce().
Rune AI

Rune AI

Key Insights

  • querySelector is the Swiss Army knife: it accepts any CSS selector and returns the first match, making it the most versatile selection method
  • getElementById is the fastest option: for single elements with IDs, it outperforms all other methods by using a direct hash table lookup
  • querySelectorAll returns static snapshots: the NodeList does not update when the DOM changes, which prevents bugs during iteration
  • getElementsByClassName returns live collections: they auto-update but can cause bugs in loops that modify matching elements
  • Always cache DOM references: repeated querySelector calls in loops waste performance, so store the result in a variable
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between querySelector and getElementById?

`querySelector("#myId")` and `getElementById("myId")` both select an element by ID, but there are differences. `getElementById` is faster because it uses the browser's internal ID hash table directly. `querySelector` is more flexible because it accepts any CSS selector, not just IDs. Note the syntax difference: `getElementById` takes a bare ID string, while `querySelector` requires the `#` prefix.

Can I use querySelectorAll to select elements from a specific parent?

Yes. Call `querySelectorAll` on any element to limit the search to its descendants: `parentElement.querySelectorAll(".child")`. This is more efficient than `document.querySelectorAll(".parent .child")` because the browser only searches the subtree. It is also safer when the page has multiple `.parent` elements with `.child` descendants.

What is the difference between a NodeList and an HTMLCollection?

`NodeList` (from `querySelectorAll`) is static and supports `.forEach()`. An `HTMLCollection` (from `getElementsByClassName`, `getElementsByTagName`) is live and updates automatically when the DOM changes. Neither has full array methods like `.map()` or `.filter()`, so convert to an array with `Array.from()` when needed.

How do I select multiple elements with different selectors?

Use comma-separated selectors in `querySelectorAll`: `document.querySelectorAll("h1, h2, h3, .title, #main-heading")`. This returns a single NodeList containing all elements matching any of the selectors, in document order. It is equivalent to running separate queries and merging the results.

Is there a performance difference between the selection methods?

Yes, but it is usually insignificant. `getElementById` is fastest (nanoseconds). `querySelector` is slightly slower due to CSS parsing. `querySelectorAll` is comparable. The live collection methods (`getElementsByClassName`, `getElementsByTagName`) are fast for initial selection but can be slower during iteration if the DOM is changing. For most applications, choose the method that produces the cleanest code.

Conclusion

JavaScript provides five built-in methods for selecting DOM elements, each suited to different scenarios. querySelector and querySelectorAll are the modern defaults that handle any CSS selector. getElementById is the speed champion for ID-based lookups. The getElementsBy* methods return live collections that auto-update with DOM changes. In practice, most codebases use querySelector/querySelectorAll for nearly everything, with getElementById reserved for performance-critical paths.