Master all DOM element selection methods in JavaScript. Compare querySelector vs getElementById, understand live HTMLCollections vs static NodeLists, and learn advanced CSS selectors with real examples.
Overview: All Selection Methods at a Glance
Before writing any code, here is the full picture of what tools you have:
DOM Selection Methods — Quick Reference:
══════════════════════════════════════════════════════════════════
Method │ Returns │ Live? │ Speed
══════════════════════════════════════════════════════════════════
getElementById('id') │ Element or null │ N/A │ ⚡ Fastest
querySelector('css') │ Element or null │ N/A │ 🔶 Fast
querySelectorAll('css') │ NodeList (static) │ No │ 🔶 Fast
getElementsByClassName('cls') │ HTMLCollection │ Yes │ 🔷 Fast
getElementsByTagName('tag') │ HTMLCollection │ Yes │ 🔷 Fast
getElementsByName('name') │ NodeList │ Yes │ 🔶 Moderate
══════════════════════════════════════════════════════════════════
<!-- Sample HTML used throughout this guide -->
<div id="container" class="wrapper">
<header class="page-header">
<nav>
<a href="/" class="nav-link active">Home</a>
<a href="/about" class="nav-link">About</a>
<a href="/contact" class="nav-link">Contact</a>
</nav>
</header>
<main>
<article data-category="tech" data-id="101">
<h2>First Article</h2>
<p class="excerpt">Tech article preview...</p>
</article>
<article data-category="science" data-id="102">
<h2>Second Article</h2>
<p class="excerpt">Science article preview...</p>
</article>
</main>
</div>
querySelector() — CSS Selector for First Match
Returns the first element matching any valid CSS selector. Incredibly flexible.
// By CLASS
const header = document.querySelector('.page-header');
console.log(header.tagName); // "HEADER"
// By TAG
const firstH2 = document.querySelector('h2');
console.log(firstH2.textContent); // "First Article"
// By ID (works, but getElementById is faster)
const cont = document.querySelector('#container');
// Compound — multiple classes
const activeLink = document.querySelector('.nav-link.active');
console.log(activeLink.textContent); // "Home"
// Descendant — element inside element
const insideMain = document.querySelector('main h2');
console.log(insideMain.textContent); // "First Article"
// Attribute selector
const techArticle = document.querySelector('[data-category="tech"]');
console.log(techArticle.dataset.id); // "101"
// Attribute starts-with
const homeLink = document.querySelector('a[href^="/"]');
console.log(homeLink.textContent); // "Home"
// Attribute contains
const aboutLink = document.querySelector('a[href*="about"]');
console.log(aboutLink.textContent); // "About"
// Pseudo-classes
const secondArticle = document.querySelector('article:nth-of-type(2)');
console.log(secondArticle.dataset.category); // "science"
// :not() selector
const nonActive = document.querySelector('.nav-link:not(.active)');
console.log(nonActive.textContent); // "About"
"HEADER"
"First Article"
"Home"
"First Article"
"101"
"Home"
"About"
"science"
"About"
querySelectorAll() — Select Multiple Elements (Static NodeList)
Returns all matching elements as a static NodeList (like a frozen snapshot).
// Select all nav links
const allLinks = document.querySelectorAll('.nav-link');
console.log(allLinks.length); // 3
console.log(allLinks instanceof NodeList); // true
// Only completed items
const techItems = document.querySelectorAll('[data-category="tech"]');
console.log(techItems.length); // 1
// NodeList supports forEach natively
allLinks.forEach((link, index) => {
console.log(`${index}: ${link.textContent} → ${link.href}`);
});
// Convert to Array for full array methods (filter, map, reduce)
const linksArray = Array.from(allLinks);
// Alternative: const linksArray = [...allLinks];
const activeLinks = linksArray.filter(l => l.classList.contains('active'));
console.log(`Active links: ${activeLinks.length}`); // 1
// ⚠️ STATIC NodeList — does NOT update when DOM changes
const excerpts = document.querySelectorAll('.excerpt');
console.log(excerpts.length); // 2
const newP = document.createElement('p');
newP.className = 'excerpt';
document.querySelector('main').appendChild(newP);
console.log(excerpts.length); // STILL 2! (snapshot, not live)
// Must re-query to get fresh results
const freshExcerpts = document.querySelectorAll('.excerpt');
console.log(freshExcerpts.length); // 3 ✅
3
true
1
0: Home → /
1: About → /about
2: Contact → /contact
"Active links: 1"
2
2
3
getElementsByClassName() — LIVE HTMLCollection
Returns a live HTMLCollection — it automatically updates when the DOM changes.
// Get all elements with 'nav-link' class
const links = document.getElementsByClassName('nav-link');
console.log(links.length); // 3
// ⚡ LIVE — automatically updates
const nav = document.querySelector('nav');
const newLink = document.createElement('a');
newLink.className = 'nav-link';
newLink.textContent = 'Blog';
nav.appendChild(newLink);
console.log(links.length); // 4 ← auto-updated! (no re-query needed)
// ❌ HTMLCollection does NOT have forEach
// links.forEach(...) → TypeError!
// ✅ Convert to Array first
Array.from(links).forEach(link => {
console.log(link.textContent);
});
// Access by index like an array
console.log(links[0].textContent); // "Home"
3
4
Home
About
Contact
Blog
"Home"
Live HTMLCollection vs Static NodeList — Critical Difference
┌──────────────────────────────────────────────────────────┐
│ LIVE HTMLCollection │
│ (getElementsByClassName, getElementsByTagName) │
│ ───────────────────────────────────────────── │
│ • Like watching a live TV broadcast │
│ • Automatically reflects DOM changes │
│ • Can cause infinite loops if modifying while iterating │
│ │
│ STATIC NodeList │
│ (querySelectorAll) │
│ ───────────────────────────────────────────── │
│ • Like a photograph taken at one moment │
│ • Does NOT reflect DOM changes after creation │
│ • Safe to iterate even while modifying DOM │
└──────────────────────────────────────────────────────────┘
const container = document.querySelector('#container');
// LIVE collection
const liveItems = document.getElementsByTagName('h2');
console.log('Live before:', liveItems.length); // 2
// STATIC snapshot
const staticItems = document.querySelectorAll('h2');
console.log('Static before:', staticItems.length); // 2
// Add a new h2
const newH2 = document.createElement('h2');
newH2.textContent = 'Third Article';
container.appendChild(newH2);
console.log('Live after:', liveItems.length); // 3 ← updated!
console.log('Static after:', staticItems.length); // 2 ← unchanged!
Live before: 2
Static before: 2
Live after: 3
Static after: 2
Scoped Queries — Search Within an Element
All selector methods can run on any element, not just document:
const main = document.querySelector('main');
// Search only INSIDE main, not the whole document
const articlesInMain = main.querySelectorAll('article');
console.log(articlesInMain.length); // 2
// getElementById only works on document, not elements
// main.getElementById('...') → TypeError!
// But querySelector works on any element
const firstExcerpt = main.querySelector('.excerpt');
console.log(firstExcerpt.textContent); // "Tech article preview..."
// Great for component-style code
function getLinks(parentEl) {
return parentEl.querySelectorAll('a');
}
const navLinks = getLinks(document.querySelector('nav'));
console.log(navLinks.length); // 3
2
"Tech article preview..."
3
CSS Selector Quick Reference for querySelector
Selector Type │ Example │ Matches
───────────────────┼────────────────────────────────┼─────────────────────────
Tag │ querySelector('p') │ First <p>
Class │ querySelector('.btn') │ First element with .btn
ID │ querySelector('#hero') │ Element with id="hero"
Multiple classes │ querySelector('.btn.primary') │ Has BOTH classes
Descendant │ querySelector('nav a') │ <a> inside <nav>
Direct child │ querySelector('ul > li') │ <li> direct child of <ul>
Adjacent sibling │ querySelector('h2 + p') │ <p> immediately after <h2>
Attribute │ querySelector('[type="email"]')│ input with type="email"
Attr contains │ querySelector('[href*="about"]')│ href has "about" anywhere
Attr starts-with │ querySelector('[href^="/"]') │ href starts with "/"
First of type │ querySelector('li:first-of-type')│ First <li>
Nth child │ querySelector('tr:nth-child(2)')│ Second <tr>
Not │ querySelector('input:not([disabled])')│ Non-disabled inputs
Real World Use Cases
// 1. Update cart count badge
const cartBadge = document.getElementById('cart-count');
cartBadge.textContent = '3';
// 2. Highlight all external links
document.querySelectorAll('a[href^="http"]').forEach(link => {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
});
// 3. Disable all submit buttons during form submission
document.querySelectorAll('button[type="submit"]').forEach(btn => {
btn.disabled = true;
btn.textContent = 'Submitting...';
});
// 4. Get all form inputs for validation
const form = document.querySelector('#signup-form');
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('error');
}
});
Common Mistakes
❌ Mistake 1: Using # in getElementById
// ❌ Wrong — # is CSS selector syntax
document.getElementById('#myDiv'); // Returns null!
// ✅ Correct
document.getElementById('myDiv'); // Returns the element
❌ Mistake 2: Calling .forEach() on HTMLCollection
// ❌ Wrong — HTMLCollection doesn't have forEach
document.getElementsByClassName('btn').forEach(b => {}); // TypeError!
// ✅ Correct — convert first, or use querySelectorAll
Array.from(document.getElementsByClassName('btn')).forEach(b => {});
document.querySelectorAll('.btn').forEach(b => {}); // NodeList has forEach ✅
❌ Mistake 3: Infinite loop with live collection
const items = document.getElementsByTagName('li'); // LIVE!
// ❌ Infinite loop! Every new li increases items.length
for (let i = 0; i < items.length; i++) {
const newLi = document.createElement('li');
document.querySelector('ul').appendChild(newLi); // items.length grows forever!
}
// ✅ Convert to static array first
const itemsArray = Array.from(items);
for (let i = 0; i < itemsArray.length; i++) {
// Safe — itemsArray.length doesn't change
}
Interview Questions
Q1. What is the difference between querySelector and getElementById? > getElementById accepts only an ID string (no #) and is the fastest selector. querySelector accepts any CSS selector, is more flexible, but slightly slower. Use getElementById when you know the ID, querySelector for complex selectors.
Q2. What is the difference between a live HTMLCollection and a static NodeList? > A live HTMLCollection (returned by getElementsByClassName, getElementsByTagName) automatically updates when the DOM changes. A static NodeList (returned by querySelectorAll) is a snapshot that doesn't change after creation.
Q3. Can querySelectorAll be called on an element (not just document)? > Yes! Any element can be used as the search root: myDiv.querySelectorAll('p') finds all <p> elements inside myDiv. This is useful for component-scoped queries.
Q4. Why might getElementsByClassName cause an infinite loop? > Because it returns a LIVE collection. If you add elements with the same class during iteration, the collection grows and the loop never ends. Convert to an array first to avoid this.
Q5. What does querySelectorAll return if nothing matches? > An empty NodeList (not null). So it's safe to call .forEach() on the result without a null check: document.querySelectorAll('.missing').forEach(...) won't throw.
Q6. Which is faster: getElementById or querySelector('#id')? > getElementById is faster because the browser maintains an internal ID hash map for O(1) lookup. querySelector must evaluate the CSS selector engine. Benchmarks show getElementById can be 2-5x faster.
Q7. How do you select elements with multiple specific classes? > Chain the classes in the selector with no space: querySelector('.btn.primary.large'). This selects elements that have ALL three classes.
Key Takeaways
✅ getElementById is fastest — use it when you have an ID
✅ querySelector is most flexible — accepts any CSS selector
✅ querySelectorAll returns STATIC NodeList — snapshot, not live
✅ getElementsByClassName/TagName return LIVE HTMLCollection
✅ NodeList has .forEach() but HTMLCollection does NOT
✅ Convert HTMLCollection to Array before using array methods
✅ querySelector can be scoped to any element, not just document
✅ querySelectorAll returns empty NodeList (not null) on no match
✅ Live collections can cause infinite loops — convert first