Home Module 10 Selecting Elements

1. Introduction

Before you can modify an element, you must first select it — find it in the DOM and get a reference to it. JavaScript provides several methods for this. The two you will use in almost every project are:

  • document.querySelector() — selects the first matching element
  • document.querySelectorAll() — selects all matching elements

Both accept CSS selectors — the same syntax you use in stylesheets. This makes them extremely powerful: any element you can style with CSS, you can also select with JavaScript.

2. Theory

2.1 querySelector — first match

// By tag name
const heading = document.querySelector('h1');

// By ID (# prefix)
const btn = document.querySelector('#submit-btn');

// By class (. prefix)
const card = document.querySelector('.card');

// By attribute
const emailInput = document.querySelector('[type="email"]');

// Complex CSS selectors
const activeLink = document.querySelector('nav a.active');
const firstLi    = document.querySelector('ul li:first-child');
const requiredInput = document.querySelector('form input[required]');

Returns: the matching Element object, or null if nothing matches.

2.2 querySelectorAll — all matches

// All paragraphs
const paras = document.querySelectorAll('p');

// All elements with a specific class
const cards = document.querySelectorAll('.card');

// All checked checkboxes
const checked = document.querySelectorAll('input[type="checkbox"]:checked');

console.log(paras.length); // number of paragraphs

// Iterate with forEach
paras.forEach(p => {
  console.log(p.textContent);
});

Returns: a NodeList (not an Array — but you can iterate it with forEach directly, or convert with Array.from()).

2.3 getElementById — by ID (fast, legacy)

const btn = document.getElementById('submit-btn');
// Note: no # prefix — just the raw ID string

Slightly faster than querySelector('#id') because browsers optimise ID lookups. In modern code, querySelector is preferred for consistency, but you will see getElementById frequently in older tutorials.

2.4 getElementsByClassName / getElementsByTagName

// By class — returns live HTMLCollection
const cards = document.getElementsByClassName('card');

// By tag — returns live HTMLCollection
const items = document.getElementsByTagName('li');

These return a live HTMLCollection — it updates automatically if the DOM changes. Modern code usually prefers querySelectorAll (returns a static NodeList snapshot).

2.5 Scoped queries — query within an element

You can call querySelector on any element, not just document. It searches only within that element's subtree:

const form = document.querySelector('#signup-form');

// These only search inside #signup-form
const emailInput = form.querySelector('[type="email"]');
const allInputs  = form.querySelectorAll('input');

console.log(allInputs.length); // inputs in this form only, not the whole page

This is important for avoiding selecting wrong elements when the same class/tag appears in multiple places on the page.

2.6 matches() — test if element matches a selector

const el = document.querySelector('.card');

el.matches('.card')         // true
el.matches('.card.featured') // false (missing 'featured' class)
el.matches('div')            // true if el is a div

// Useful in event delegation (Module 11)
document.addEventListener('click', e => {
  if (e.target.matches('.card button')) {
    console.log('Card button clicked!');
  }
});

2.7 closest() — find nearest ancestor matching a selector

// Starting from a button inside a card, find the card element
const btn = document.querySelector('.card button');
const card = btn.closest('.card');
console.log(card); // the .card ancestor

// Walk up to find the nearest form
input.closest('form')

// Returns null if no ancestor matches
el.closest('.non-existent') // null

2.8 NodeList vs Array

const items = document.querySelectorAll('li');

// Works — NodeList has forEach
items.forEach(item => console.log(item.textContent));

// Does NOT work — NodeList has no map/filter/reduce
items.map(item => item.textContent); // TypeError

// Convert to a real array first
const arr = Array.from(items);
arr.map(item => item.textContent); // works now

// Or spread
const arr2 = [...items];
arr2.filter(item => item.textContent.includes('a'));

2.9 Quick reference table

MethodReturnsUse when
querySelector(sel)Element or nullNeed first/only match
querySelectorAll(sel)Static NodeListNeed all matches
getElementById(id)Element or nullSelect by ID (legacy)
getElementsByClassName(cls)Live HTMLCollectionRarely needed now
getElementsByTagName(tag)Live HTMLCollectionRarely needed now
el.matches(sel)booleanCheck if element matches
el.closest(sel)Element or nullFind nearest ancestor

3. Real World Example

// A "Select All" checkbox that toggles all item checkboxes
const selectAll  = document.querySelector('#select-all');
const checkboxes = document.querySelectorAll('.item-checkbox');

selectAll.addEventListener('change', () => {
  checkboxes.forEach(cb => {
    cb.checked = selectAll.checked;
  });
});

// Highlight active nav link
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
  if (link.href === window.location.href) {
    link.classList.add('active');
  }
});

// Find which card a "Delete" button belongs to
document.querySelectorAll('.delete-btn').forEach(btn => {
  btn.addEventListener('click', () => {
    const card = btn.closest('.card');
    card.remove();
  });
});

4. Code Example

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Selecting Elements</title></head>
<body>
  <nav>
    <a href="#" class="nav-link active">Home</a>
    <a href="#" class="nav-link">About</a>
    <a href="#" class="nav-link">Contact</a>
  </nav>

  <form id="search-form">
    <input type="text" id="search-input" placeholder="Search...">
    <button type="submit">Go</button>
  </form>

  <ul id="results">
    <li class="result">JavaScript basics</li>
    <li class="result featured">DOM manipulation</li>
    <li class="result">CSS grid</li>
  </ul>

  <button id="demo-btn">Run Selection Demo</button>
  <pre id="output"></pre>

  <script>
    document.querySelector('#demo-btn').addEventListener('click', () => {
      const lines = [];

      // querySelector — first match
      const active = document.querySelector('.nav-link.active');
      lines.push('Active link: ' + active.textContent);

      // querySelectorAll — all matches
      const results = document.querySelectorAll('.result');
      lines.push('Result count: ' + results.length);
      results.forEach((li, i) => lines.push(`  [${i}] ${li.textContent}`));

      // Scoped query — search within the form only
      const form       = document.querySelector('#search-form');
      const searchInput = form.querySelector('input');
      lines.push('Search input type: ' + searchInput.type);

      // matches()
      const featured = document.querySelector('.result.featured');
      lines.push('Is featured? ' + featured.matches('.featured'));
      lines.push('Is a link? '   + featured.matches('a'));

      // closest()
      const btn = document.querySelector('button[type="submit"]');
      const parentForm = btn.closest('form');
      lines.push('Button form id: ' + parentForm.id);

      // Convert NodeList to Array
      const arr = Array.from(results);
      const texts = arr.map(li => li.textContent);
      lines.push('Mapped: ' + texts.join(', '));

      document.querySelector('#output').textContent = lines.join('\n');
    });
  </script>
</body>
</html>

5. Code Breakdown

'.nav-link.active' — compound class selector

Two classes with no space between them means the element must have both classes. This is the same CSS syntax you already know — no new learning required for the selector language.

results.forEach((li, i) => ...)

NodeList's forEach works like Array's. The callback receives each element and optionally its index. Unlike arrays, you cannot use results[0] to get the first item... actually you can — NodeLists do support bracket notation. But for looping, forEach is cleaner.

form.querySelector('input')

Calling querySelector on an element restricts the search to that element's descendants. If there are inputs elsewhere on the page, they will not be returned — only inputs inside #search-form.

Array.from(results).map(...)

Converting a NodeList to an array unlocks all array methods. map creates a new array of transformed values — here, extracting the textContent string from each element.

6. Common Mistakes

Mistake 1 — querySelector returns null, then you access a property

const el = document.querySelector('#missing');
el.textContent = 'hi'; // TypeError: Cannot set properties of null

// Fix
const el = document.querySelector('#missing');
if (el) el.textContent = 'hi';

Mistake 2 — Calling array methods on a NodeList

const items = document.querySelectorAll('li');
items.map(x => x.textContent); // TypeError — NodeList has no .map

// Fix
Array.from(items).map(x => x.textContent);
// or
[...items].map(x => x.textContent);

Mistake 3 — getElementById with # prefix

// Wrong — getElementById doesn't use #
document.getElementById('#my-id'); // returns null

// Correct
document.getElementById('my-id'); // no #

Mistake 4 — Not scoping queries

// If multiple forms exist, this picks the wrong input
const input = document.querySelector('input[type="text"]');

// Better — scope to the relevant container
const form  = document.querySelector('#login-form');
const input = form.querySelector('input[type="text"]');

Mistake 5 — Live HTMLCollection causing infinite loops

// getElementsByTagName returns a LIVE collection
const items = document.getElementsByTagName('li');
for (let i = 0; i < items.length; i++) {
  list.appendChild(document.createElement('li')); // items.length grows!
  // This can loop forever
}

// Use querySelectorAll (static snapshot) instead
const items = document.querySelectorAll('li');

7. Best Practices

  1. Use querySelector/querySelectorAll as your default — consistent, powerful, and familiar CSS syntax.
  2. Cache selections in const variablesconst btn = document.querySelector('#btn') — avoid re-querying the DOM repeatedly.
  3. Scope queries to a container when possible — form.querySelector('input') is safer than document.querySelector('input').
  4. Always check for null before accessing properties of a potentially missing element.
  5. Convert NodeLists to arrays with Array.from() or [...nodeList] when you need map/filter/reduce.
  6. Use closest() to navigate from a clicked button to its parent container — much cleaner than nested parentElement.parentElement.
  7. Use matches() for event delegation checks instead of reading class lists or tag names manually.

8. Practice Exercise

  1. Create an HTML page with: a navigation with 4 links (one with class "active"), a grid of 6 cards (each with title and description), and a form with various input types.
  2. Write script that selects and logs:
    • The active nav link text
    • All card titles (use querySelectorAll + forEach)
    • All required form inputs
    • The text of the 3rd card using querySelectorAll with index [2]
  3. Add a "highlight" button — when clicked, add a yellow background to all cards using querySelectorAll + forEach.
  4. Use closest(): add a delete button inside each card. When a delete button is clicked, use closest('.card') to find and log the card's title.

9. Assignment

Build a "Live Search Filter."

  1. Create a page with a text input and a list of 10 items (names, foods, countries — your choice).
  2. As the user types in the input, filter the list — hide items that don't match the typed text (case-insensitive).
  3. Implementation steps:
    • Select the input and all list items with appropriate selection methods.
    • On each input event, get the current value.
    • Loop over all list items. For each item, check if its text includes the search term.
    • Set style.display = 'none' or '' to show/hide.
  4. Show a "No results found" message when all items are hidden.
  5. Highlight the matching text within each visible item (wrap with <mark> using innerHTML — but be careful to sanitise first).

Deliverable: One HTML file. No libraries — pure JavaScript DOM manipulation.

10. Interview Questions

  1. What is the difference between querySelector and querySelectorAll?
    querySelector returns the first element that matches the CSS selector, or null. querySelectorAll returns a NodeList of all matching elements (empty NodeList if none match).
  2. What is the difference between a NodeList and an HTMLCollection?
    HTMLCollection (from getElementsByClassName/TagName) is live — it updates when the DOM changes. NodeList (from querySelectorAll) is a static snapshot. NodeList supports forEach directly; HTMLCollection does not.
  3. How do you get an array of matching elements so you can use map/filter?
    Convert the NodeList: Array.from(document.querySelectorAll('li')) or [...document.querySelectorAll('li')].
  4. What does element.closest() do?
    Starting from the element, it walks up the DOM tree and returns the nearest ancestor that matches the selector. Returns null if no ancestor matches. Useful for finding a parent container from a deeply nested event target.
  5. When would you scope a querySelector to a container element instead of document?
    When the same selector (e.g., 'input') exists in multiple places on the page and you only want elements within a specific section. container.querySelector('input') restricts the search to that container's subtree.

11. Additional Resources

  • MDN — Document.querySelector()
  • MDN — Document.querySelectorAll()
  • MDN — Element.closest()
  • MDN — Element.matches()
  • javascript.info — Searching: getElement*, querySelector*