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.

JavaScriptbeginner
10 min read

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:

javascriptjavascript
// 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); // null

Always check for null before using the result, since the element might not exist in the DOM:

javascriptjavascript
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:

javascriptjavascript
// 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); // 0

Unlike 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

javascriptjavascript
// 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

javascriptjavascript
// 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 h2

Attribute Selectors

javascriptjavascript
// 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

javascriptjavascript
// 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)

javascriptjavascript
const items = document.querySelectorAll(".item");
 
items.forEach((item, index) => {
  console.log(`Item ${index}: ${item.textContent}`);
});

for...of Loop

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
// 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):

javascriptjavascript
// 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

javascriptjavascript
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 ID

matches() is useful in event handlers where you need to check if the clicked element matches a pattern:

javascriptjavascript
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

javascriptjavascript
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"); // null

closest() is extremely useful for event delegation:

javascriptjavascript
// 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:

javascriptjavascript
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

ApproachRelative SpeedWhen to Use
getElementByIdFastestKnown IDs in hot paths
querySelector (simple)FastSingle element, any selector
querySelector (complex)ModerateNested/compound selectors
querySelectorAllModerateMultiple elements
Scoped query on cached parentFastRepeated lookups in a section
javascriptjavascript
// 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

javascriptjavascript
// 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

javascriptjavascript
// 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 IDs

Running querySelectorAll in a Loop

javascriptjavascript
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

  1. Use querySelector for single elements and querySelectorAll for multiple. Do not use querySelectorAll when you only need the first match.
  2. Scope queries to parent elements for precision and performance. parent.querySelector(".child") is better than document.querySelector(".parent .child").
  3. Cache references in variables. Never call querySelector repeatedly for the same element, especially inside event handlers or loops.
  4. Use closest() for event delegation instead of walking up the tree manually.
  5. Convert NodeLists to arrays with Array.from() or [...nodeList] when you need .map(), .filter(), or .reduce().
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

What happens if querySelector finds no match?

`querySelector` returns `null`. If you try to access properties on `null` (like `.textContent` or `.classList`), you get a `TypeError`. Always check for `null` with an `if` statement or use [optional chaining (`?.`)](/tutorials/programming-languages/javascript/js-optional-chaining-syntax-complete-guide): `document.querySelector(".item")?.textContent`.

Can I use querySelector with complex CSS selectors?

Yes. Both `querySelector` and `querySelectorAll` support the full range of CSS selectors: combinators (`>`, `+`, `~`), pseudo-classes (`:nth-child`, `:not`, `:first-of-type`), attribute selectors (`[data-id="5"]`), and comma-separated groups (`"h1, h2, h3"`). If the selector is valid in a CSS stylesheet, it works in `querySelector`.

Is querySelectorAll the same as getElementsByClassName?

No. `querySelectorAll` returns a static NodeList (a snapshot), while `getElementsByClassName` returns a live HTMLCollection that updates automatically when the DOM changes. `querySelectorAll` also accepts any CSS selector, while `getElementsByClassName` only accepts class names. For most use cases, `querySelectorAll` is the better choice.

How do I select elements with multiple classes?

Chain the class selectors without spaces: `document.querySelectorAll(".card.active.highlighted")`. This selects elements that have all three classes. If you want elements with any of the classes, use commas: `document.querySelectorAll(".card, .active, .highlighted")`.

Can I use querySelector on elements other than document?

Yes. Any element supports `querySelector` and `querySelectorAll`. Calling them on an element limits the search to that element's descendants: `myDiv.querySelectorAll("p")` finds only `<p>` elements inside `myDiv`. This is faster and prevents selecting unintended elements elsewhere on the page.

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.