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.
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:
- Modern query methods:
querySelectorandquerySelectorAlluse CSS selector syntax - 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:
// 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:
// 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:
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:
// 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:
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:
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"); // CORRECTKey detail: getElementById is only available on the document object, not on individual elements. IDs must be unique per page.
// 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:
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" classesLive Collection vs Static NodeList
getElementsByClassName returns a live collection. This means it automatically updates when the DOM changes:
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 countWarning: Live collections can cause bugs in loops that add or remove elements matching the selector:
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:
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
| Method | Returns | Static/Live | CSS Selectors | Speed |
|---|---|---|---|---|
querySelector | Single element or null | Static | Full CSS support | Fast |
querySelectorAll | NodeList | Static | Full CSS support | Fast |
getElementById | Single element or null | N/A | ID only | Fastest |
getElementsByClassName | HTMLCollection | Live | Class only | Very fast |
getElementsByTagName | HTMLCollection | Live | Tag only | Very fast |
When to Use Each Method
// 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/removedReal-World Example: Dynamic Form Validator
Here is a practical example combining multiple selection methods:
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
// 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
// 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
// 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
// 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
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
// 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
- Use
querySelector/querySelectorAllas your default. They handle any selector pattern with consistent, familiar CSS syntax. - Use
getElementByIdfor ID-based lookups when performance matters. It is the fastest single-element selection method. - Cache DOM references in variables. Never query the same element repeatedly, especially inside loops.
- Scope queries to parent elements when possible.
parent.querySelector(".child")is faster and more precise thandocument.querySelector(".parent .child"). - Convert NodeLists to arrays with
Array.from()or spread ([...nodeList]) when you need.map(),.filter(), or.reduce().
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
querySelectorcalls in loops waste performance, so store the result in a variable
Frequently Asked Questions
What is the difference between querySelector and getElementById?
Can I use querySelectorAll to select elements from a specific parent?
What is the difference between a NodeList and an HTMLCollection?
How do I select multiple elements with different selectors?
Is there a performance difference between the selection methods?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.