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.
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>
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.
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.
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"].
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>
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.
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 matchingnameattribute, 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.
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.
Summary
Six main methods search the DOM for elements:
| Method | Searches by… | Call on an element? | Live? |
querySelector | CSS selector | ✔ | – |
querySelectorAll | CSS selector | ✔ | – |
getElementById | id | – | – |
getElementsByName | name | – | ✔ |
getElementsByTagName | tag or ‘*’ | ✔ | ✔ |
getElementsByClassName | class | ✔ | ✔ |
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? Returnstrueorfalse.elem.closest(css)— walk up fromelem(itself included) and return the nearest ancestor that matches, ornull.
And one more, useful for parent–child relationship checks:
elemA.contains(elemB)returnstruewhenelemBsits insideelemA(a descendant), and also whenelemA === elemB.