Searching: getElement*, querySelector*

Navigation properties like parentNode and children work great when the element you want sits right next to one you already have. Step from a parent to its child, hop to a sibling — that’s fine for short trips. But most of the time the element you need is somewhere else entirely, buried in a part of the page you have no handle on yet.

For that you need search methods: give the browser a description of what you’re looking for, and it finds the matching node (or nodes) anywhere in the document.

navigation: one hop at a timeyouparentsiblingtargetsearch: straight to the matchyoutarget
Navigation walks step by step from a known node. Searching jumps straight to a match anywhere in the tree.

document.getElementById or just id

Give an element an id attribute and you can grab it from anywhere with document.getElementById(id). Position in the tree doesn’t matter — the browser keeps an internal index of ids, so this is a fast, direct lookup.

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // get the element
  let elem = document.getElementById('elem');

  // make its background red
  elem.style.background = 'red';
</script>

There’s also a quirk baked into browsers: every element with an id becomes a global variable of the same name, pointing at that element.

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // elem is a reference to the DOM element with id="elem"
  elem.style.background = 'red';

  // id="elem-content" has a hyphen, so it can't be a plain variable name
  // ...but you can still read it as window['elem-content']
</script>

This auto-global only wins when nothing else claims the name. Declare a real JavaScript variable called elem and it shadows the element reference:

<div id="elem"></div>

<script>
  let elem = 5; // now elem is 5, not a reference to <div id="elem">

  alert(elem); // 5
</script>
window.elemlet elem = 5<div id=“elem”>auto-globalshadowed → holds 5, not the div
Two names can point at the same element. A user-declared variable overrides the browser's auto-global.

querySelectorAll

elem.querySelectorAll(css) is the workhorse. Hand it any CSS selector and it returns every element inside elem that matches.

Here we pull the last <li> from each list:

<ul>
  <li>Tea</li>
  <li>Coffee</li>
</ul>
<ul>
  <li>Juice</li>
  <li>Water</li>
</ul>
<script>
  let elements = document.querySelectorAll('ul > li:last-child');

  for (let elem of elements) {
    alert(elem.innerHTML); // "Coffee", "Water"
  }
</script>

The power comes from the selector itself. Anything you can write in a stylesheet — descendant combinators, attribute matches, :nth-child, comma-separated groups — works here too.

ulli — Teali — Coffee ✓ulli — Juiceli — Water ✓returns [ <li>Coffee</li>, <li>Water</li> ]
The selector 'ul > li:last-child' matches only the final direct li of each ul.

Try it for yourself. Type any CSS selector below and watch which elements light up. Edit the code to change the markup or the default selector, then experiment with :nth-child(2), li:first-child, or a comma group like li:first-child, li:last-child.

interactiveSelector explorer

querySelector

elem.querySelector(css) returns the first match for the selector, or null if nothing matches.

The result is the same element you’d get from elem.querySelectorAll(css)[0], but the two do different work under the hood. querySelectorAll finds every match and builds a collection, then you index into it. querySelector stops at the first hit. It’s shorter to type and does less work, so prefer it whenever you only need one element.

matches

The search methods above go looking through the DOM. elem.matches(css) does the opposite: it searches nothing. It just answers a yes/no question — does this one element match the selector? — and returns true or false.

That’s handy when you already have a collection of elements and want to filter it down to the ones that fit a pattern:

<a href="https://files.dev/backup.zip">...</a>
<a href="https://files.dev/dashboard">...</a>

<script>
  // could be any collection instead of document.body.children
  for (let elem of document.body.children) {
    if (elem.matches('a[href$="zip"]')) {
      alert("The archive reference: " + elem.href );
    }
  }
</script>

The selector a[href$="zip"] reads as “an <a> whose href ends with zip”. Loop over the children, ask each one matches, act on the ones that say true.

The demo below runs exactly that filter over a mixed list of links. Press the button to keep only the archive links, and edit the selector in the code to filter by something else, like a[href$="pdf"].

interactiveFiltering with matches

closest

The ancestors of an element are its parent, then that parent’s parent, and so on up to the root. Together they form a chain from the element to the top of the tree.

elem.closest(css) walks up that chain and returns the nearest node that matches the selector. The search starts with elem itself, then its parent, then the parent’s parent, stopping at the first match. If nothing in the chain matches, you get null.

<h1>Menu</h1>

<div class="menu">
  <ul class="section">
    <li class="dish">Soup</li>
    <li class="dish">Salad</li>
  </ul>
</div>

<script>
  let dish = document.querySelector('.dish'); // LI

  alert(dish.closest('.section')); // UL
  alert(dish.closest('.menu')); // DIV

  alert(dish.closest('h1')); // null (h1 is not an ancestor)
</script>
li.dish (start)ul.sectiondiv.menuh1closest(‘.section’)closest(‘.menu’)not an ancestor → null
closest climbs the ancestor chain from the element upward, returning the first selector match. h1 is a sibling branch, so it's never reached.

Pick a selector and watch closest climb from the highlighted li upward. A match on the chain is reported by tag; a selector that names a non-ancestor (like h1) returns null.

interactiveClimbing with closest

getElementsBy*

An older family of methods searches by tag, class, or name. querySelector and querySelectorAll cover everything they do with shorter, more flexible code, so these are mostly legacy now. You’ll still meet them in existing scripts, and one of their traits (see live collections below) is worth knowing.

  • elem.getElementsByTagName(tag) returns a collection of elements with the given tag. Pass "*" to match every tag.
  • elem.getElementsByClassName(className) returns elements carrying the given CSS class.
  • document.getElementsByName(name) returns elements with a matching name attribute, across the whole document. Rarely needed.
// every div in the document
let divs = document.getElementsByTagName('div');

Here we collect every input inside a table:

<table id="table">
  <tr>
    <td>Your plan:</td>

    <td>
      <label>
        <input type="radio" name="plan" value="free" checked> Free
      </label>
      <label>
        <input type="radio" name="plan" value="pro"> Pro
      </label>
      <label>
        <input type="radio" name="plan" value="team"> Team
      </label>
    </td>
  </tr>
</table>

<script>
  let inputs = table.getElementsByTagName('input');

  for (let input of inputs) {
    alert( input.value + ': ' + input.checked );
  }
</script>

Searching by name, then by class inside the result:

<form name="signup">
  <div class="field">Name</div>
  <div class="required field">Email</div>
</form>

<script>
  // find by name attribute
  let form = document.getElementsByName('signup')[0];

  // find by class inside the form
  let fields = form.getElementsByClassName('field');
  alert(fields.length); // 2, found two elements with class "field"
</script>

Notice the second <div> has class="required field". getElementsByClassName('field') matches any element whose class list includes field, so both divs count.

Live collections

Every getElementsBy* method returns a live collection. It stays wired to the document: add or remove matching elements later, and the collection updates on its own.

The example below has two scripts. The first grabs the collection of <div> when only one exists. By the time the second script runs, the browser has parsed a second <div>, and the same collection already reflects it.

<div>First div</div>

<script>
  let divs = document.getElementsByTagName('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 2
</script>

querySelectorAll behaves differently: it returns a static collection, a frozen snapshot taken at the moment of the call. It’s like a fixed array. Swap it in and both scripts report 1:

<div>First div</div>

<script>
  let divs = document.querySelectorAll('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 1
</script>

The snapshot didn’t grow when the new div showed up.

after a 2nd <div> is addedgetElementsByTagName (live)div — Firstdiv — Second (added)length = 2querySelectorAll (static)div — First(new div not included)length = 1
A live collection tracks the document as it changes; a static one is frozen at call time.

See both behaviors side by side. Both collections are captured once, up front. Each time you add a <div>, the live one grows while the static snapshot stays put.

interactiveLive vs. static collection

Summary

Six main methods search the DOM for elements:

MethodSearches by…Call on an element?Live?
querySelectorCSS selector
querySelectorAllCSS selector
getElementByIdid
getElementsByNamename
getElementsByTagNametag or ‘*’
getElementsByClassNameclass

querySelector and querySelectorAll do the heavy lifting in day-to-day code. The getElement(s)By* methods turn up occasionally, and in older scripts.

Two more methods that check rather than search:

  • elem.matches(css) — does this element match the selector? Returns true or false.
  • elem.closest(css) — walk up from elem (itself included) and return the nearest ancestor that matches, or null.

And one more, useful for parent–child relationship checks:

  • elemA.contains(elemB) returns true when elemB sits inside elemA (a descendant), and also when elemA === elemB.