How to Use JS querySelector and querySelectorAll
Master JavaScript querySelector and querySelectorAll methods. Learn CSS selector syntax, scoped queries, NodeList iteration, and real-world selection patterns.
querySelector and querySelectorAll are the two most important DOM selection methods in modern JavaScript. They use CSS selector syntax to find elements, making them both powerful and familiar to anyone who writes CSS. Whether you need a single element or every element matching a pattern, these two methods cover virtually every selection scenario.
This guide covers every CSS selector pattern supported by these methods, iteration techniques, scoped queries, and the performance characteristics you need to know.
querySelector Basics
querySelector returns the first element matching the CSS selector, or null if no element matches:
// Select by tag
const heading = document.querySelector("h1");
// Select by class
const card = document.querySelector(".card");
// Select by ID
const app = document.querySelector("#app");
// Select by attribute
const required = document.querySelector("[required]");
const emailInput = document.querySelector('input[type="email"]');
// Returns null when nothing matches
const ghost = document.querySelector(".nonexistent");
console.log(ghost); // nullAlways check for null before using the result, since the element might not exist in the DOM:
const element = document.querySelector(".dynamic-content");
if (element) {
element.textContent = "Found it!";
}
// Or use optional chaining for simple operations
document.querySelector(".status")?.classList.add("ready");querySelectorAll Basics
querySelectorAll returns a static NodeList of all matching elements:
// All paragraphs
const paragraphs = document.querySelectorAll("p");
console.log(paragraphs.length);
// All elements with class "card"
const cards = document.querySelectorAll(".card");
// All checked checkboxes
const checked = document.querySelectorAll('input[type="checkbox"]:checked');
// Empty NodeList when nothing matches (not null)
const empty = document.querySelectorAll(".nonexistent");
console.log(empty.length); // 0Unlike querySelector which returns null, querySelectorAll always returns a NodeList (which may be empty). You can safely call .forEach() on an empty NodeList without errors.
CSS Selector Patterns
These methods accept any valid CSS selector. Here are the most useful patterns:
Basic Selectors
// Type selector
document.querySelector("div");
// Class selector
document.querySelector(".card");
// ID selector
document.querySelector("#main");
// Universal selector
document.querySelectorAll("*"); // Every element
// Multiple classes (AND - must have both)
document.querySelector(".card.active");
// Multiple selectors (OR - matches any)
document.querySelectorAll("h1, h2, h3");Combinators
// Descendant (any depth)
document.querySelector("article p");
// Direct child
document.querySelector("ul > li");
// Adjacent sibling (immediately follows)
document.querySelector("h2 + p"); // First p after h2
// General sibling (any following sibling)
document.querySelectorAll("h2 ~ p"); // All p's after h2Attribute Selectors
// Has attribute
document.querySelectorAll("[data-id]");
// Exact value
document.querySelector('[type="email"]');
// Starts with
document.querySelectorAll('[href^="https"]');
// Ends with
document.querySelectorAll('[src$=".png"]');
// Contains
document.querySelectorAll('[class*="btn"]');Pseudo-Classes
// First/last child
document.querySelector("li:first-child");
document.querySelector("li:last-child");
// nth child
document.querySelectorAll("tr:nth-child(even)");
document.querySelectorAll("li:nth-child(3n)");
// First/last of type
document.querySelector("p:first-of-type");
// Not selector
document.querySelectorAll("input:not([type='hidden'])");
// Checked state
document.querySelectorAll("input:checked");
// Disabled/enabled
document.querySelectorAll("button:disabled");
// Empty elements
document.querySelectorAll("div:empty");Iterating Over Results
forEach (Direct)
const items = document.querySelectorAll(".item");
items.forEach((item, index) => {
console.log(`Item ${index}: ${item.textContent}`);
});for...of Loop
const cards = document.querySelectorAll(".card");
for (const card of cards) {
card.classList.add("animated");
}Convert to Array for Full Methods
The NodeList from querySelectorAll supports .forEach() and for...of, but it does not have .map(), .filter(), or .reduce(). Convert to an array first:
const items = document.querySelectorAll(".product");
// Get all prices as numbers
const prices = Array.from(items).map(item => {
return parseFloat(item.querySelector(".price").textContent);
});
// Filter to items over $50
const expensive = [...items].filter(item => {
const price = parseFloat(item.querySelector(".price").textContent);
return price > 50;
});
// Sum all prices
const total = Array.from(items).reduce((sum, item) => {
return sum + parseFloat(item.querySelector(".price").textContent);
}, 0);Scoped Queries
Calling querySelector or querySelectorAll on an element limits the search to that element's descendants:
// Search only inside the sidebar
const sidebar = document.querySelector(".sidebar");
const sidebarLinks = sidebar.querySelectorAll("a");
const activeItem = sidebar.querySelector(".active");
// Search only inside a specific form
const loginForm = document.querySelector("#login-form");
const emailField = loginForm.querySelector('input[type="email"]');
const submitBtn = loginForm.querySelector('button[type="submit"]');Scoped queries are faster (smaller search area) and more precise (no accidental matches from other parts of the page):
// Without scoping - might grab the wrong heading
const heading = document.querySelector(".section h2");
// With scoping - guaranteed to be in the right section
const section = document.querySelector(".section");
const heading = section.querySelector("h2");matches() and closest()
Two related methods complement querySelector:
matches() - Test If an Element Matches a Selector
const element = document.querySelector(".card");
console.log(element.matches(".card")); // true
console.log(element.matches(".card.active")); // true if has both classes
console.log(element.matches("div")); // true if it's a <div>
console.log(element.matches("#specific-id")); // depends on the IDmatches() is useful in event handlers where you need to check if the clicked element matches a pattern:
document.addEventListener("click", event => {
if (event.target.matches("button.delete")) {
handleDelete(event.target);
}
if (event.target.matches("a.external")) {
handleExternalLink(event.target);
}
});closest() - Find the Nearest Matching Ancestor
const link = document.querySelector("nav a");
// Find the closest ancestor matching the selector (or self)
const nav = link.closest("nav");
const header = link.closest("header");
const container = link.closest(".container");
// Returns null if no ancestor matches
const footer = link.closest("footer"); // nullclosest() is extremely useful for event delegation:
// Handle clicks on any card, even clicking nested elements
document.addEventListener("click", event => {
const card = event.target.closest(".card");
if (card) {
const cardId = card.dataset.id;
console.log(`Clicked card: ${cardId}`);
}
});Real-World Example: Tab Component
Here is a practical example building a tab component using query selectors:
function initTabs(containerSelector) {
const container = document.querySelector(containerSelector);
if (!container) return;
const tabButtons = container.querySelectorAll("[data-tab]");
const tabPanels = container.querySelectorAll("[data-panel]");
function activateTab(tabId) {
// Deactivate all tabs
tabButtons.forEach(btn => {
btn.classList.remove("active");
btn.setAttribute("aria-selected", "false");
});
tabPanels.forEach(panel => {
panel.classList.remove("active");
panel.setAttribute("hidden", "");
});
// Activate the selected tab
const activeButton = container.querySelector(`[data-tab="${tabId}"]`);
const activePanel = container.querySelector(`[data-panel="${tabId}"]`);
if (activeButton && activePanel) {
activeButton.classList.add("active");
activeButton.setAttribute("aria-selected", "true");
activePanel.classList.add("active");
activePanel.removeAttribute("hidden");
}
}
// Add click handlers to each tab button
tabButtons.forEach(btn => {
btn.addEventListener("click", () => {
activateTab(btn.dataset.tab);
});
});
// Activate the first tab by default
const firstTab = tabButtons[0];
if (firstTab) {
activateTab(firstTab.dataset.tab);
}
}
initTabs(".tabs-container");This example uses scoped queries (container.querySelector) to ensure the tab component only affects elements within its own container, allowing multiple tab components on the same page.
Performance Considerations
| Approach | Relative Speed | When to Use |
|---|---|---|
getElementById | Fastest | Known IDs in hot paths |
querySelector (simple) | Fast | Single element, any selector |
querySelector (complex) | Moderate | Nested/compound selectors |
querySelectorAll | Moderate | Multiple elements |
| Scoped query on cached parent | Fast | Repeated lookups in a section |
// Cache parent for repeated queries
const form = document.getElementById("checkout-form");
// These scoped queries are fast because the search area is small
const nameField = form.querySelector("#name");
const emailField = form.querySelector("#email");
const submitBtn = form.querySelector("button[type='submit']");Common Mistakes to Avoid
Forgetting That querySelector Returns Only the First Match
// Only styles the FIRST card, not all of them
document.querySelector(".card").style.border = "1px solid blue";
// Fix: use querySelectorAll
document.querySelectorAll(".card").forEach(card => {
card.style.border = "1px solid blue";
});Using Invalid CSS Selectors
// These throw SyntaxError
// document.querySelector("div[data=123]"); // Unquoted attribute value
// document.querySelector("#123abc"); // IDs can't start with digits in CSS
// Correct
document.querySelector('div[data="123"]');
document.querySelector('[id="123abc"]'); // Use attribute selector for numeric IDsRunning querySelectorAll in a Loop
const container = document.querySelector(".list");
// BAD: runs querySelectorAll 100 times
for (let i = 0; i < 100; i++) {
document.querySelectorAll(".item")[i]?.remove();
}
// GOOD: query once, then iterate
const items = container.querySelectorAll(".item");
items.forEach(item => item.remove());Best Practices
- Use
querySelectorfor single elements andquerySelectorAllfor multiple. Do not usequerySelectorAllwhen you only need the first match. - Scope queries to parent elements for precision and performance.
parent.querySelector(".child")is better thandocument.querySelector(".parent .child"). - Cache references in variables. Never call
querySelectorrepeatedly for the same element, especially inside event handlers or loops. - Use
closest()for event delegation instead of walking up the tree manually. - Convert NodeLists to arrays with
Array.from()or[...nodeList]when you need.map(),.filter(), or.reduce().
Rune AI
Key Insights
- querySelector returns one element or null: always check for null before using the result to avoid TypeError crashes
- querySelectorAll returns a static NodeList: it is a snapshot that does not update when the DOM changes, which prevents iteration bugs
- CSS selector syntax applies: combinators, pseudo-classes, attribute selectors, and comma groups all work as they do in stylesheets
- Scope queries to parent elements:
parent.querySelector(".child")is faster and more precise than searching the entire document - Use closest() for upward traversal: it finds the nearest ancestor matching a selector, which is essential for event delegation patterns
Frequently Asked Questions
What happens if querySelector finds no match?
Can I use querySelector with complex CSS selectors?
Is querySelectorAll the same as getElementsByClassName?
How do I select elements with multiple classes?
Can I use querySelector on elements other than document?
Conclusion
querySelector and querySelectorAll are the foundation of modern DOM selection in JavaScript. Their CSS selector syntax makes them powerful enough to handle any selection pattern while remaining familiar to anyone who writes stylesheets. Combined with matches() for testing and closest() for upward traversal, they provide a complete toolkit for finding and working with DOM elements.
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.