JavaScript Selecting DOM Elements
Learn how to select DOM elements in JavaScript with getElementById, querySelector, querySelectorAll, getElementsByClassName, matches, and closest.
Before you can change anything on a page with JavaScript — update text, toggle a class, attach a click handler — you first have to find the element you want. That act of finding is called selecting a DOM element. This chapter covers every method you need, explains what each one returns, and shows when to reach for which.
What "selecting an element" means
The Document Object Model (DOM) is the browser's live, tree-shaped representation of your HTML. Every tag becomes a node object you can read and modify from JavaScript. Selecting is the process of locating one or more of those nodes so you can hold a reference to them in a variable:
const heading = document.querySelector('h1');
// `heading` now points at the real <h1> on the page.Once you have that reference, everything else — reading attributes and properties, changing styles, adding event listeners — works on it.
The selection methods fall into two groups:
- ID / class / tag lookups — fast, return live collections (except
getElementById). - CSS-selector lookups (
querySelector,querySelectorAll) — flexible, accept any CSS selector, return a static result.
Selecting one element by ID: getElementById
The fastest way to grab a single element is by its unique id. It returns the element, or null if no element with that ID exists.
const element = document.getElementById('example');
element.textContent = 'You selected the element by its ID!';Because IDs are meant to be unique, getElementById returns a single element (not a collection) and is the most direct, best-performing lookup.
If no element matches, getElementById returns null. Calling a property on null throws TypeError: Cannot read properties of null. Guard against it: const el = document.getElementById('maybe'); if (el) { /* ... */ }.
Selecting by class name: getElementsByClassName
This method returns a live HTMLCollection of every element carrying the given class. "Live" means the collection updates automatically as the DOM changes.
const elements = document.getElementsByClassName('example');
Array.from(elements).forEach((element, index) => {
element.textContent = `Element ${index + 1} changed!`;
});An HTMLCollection is array-like but not a real array, so it has no forEach/map. Convert it with Array.from() (or spread [...elements]) before using array methods.
Because the collection is live, looping with for (let i = 0; i < c.length; i++) while removing matching elements can skip items — the length shrinks under you. Snapshot it first with Array.from() when you plan to mutate the DOM during the loop.
Selecting by tag name: getElementsByTagName
Selects every element with a given tag name and returns a live HTMLCollection.
const paragraphs = document.getElementsByTagName('p');
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.backgroundColor = 'yellow';
}Pass '*' to match every element on the page. This is handy when you want to operate on all tags of one kind, such as highlighting every paragraph.
Selecting by name attribute: getElementsByName
Returns a live NodeList of elements that share the same name attribute. It is most useful for form controls — for example, all radio buttons in one group:
const options = document.getElementsByName('plan');
options.forEach((radio) => {
radio.addEventListener('change', () => {
console.log('Selected plan:', radio.value);
});
});For deeper form handling, see working with forms in the DOM.
Selecting with CSS selectors: querySelector
querySelector returns the first element that matches any CSS selector you pass, or null if nothing matches. This is the most versatile single-element method.
const element = document.querySelector('.example');
element.style.backgroundColor = 'lightblue';Because it accepts the full CSS selector syntax, you can target deeply: querySelector('nav ul li.active a') finds the first matching link without chaining several calls.
Selecting all matches: querySelectorAll
Returns a static NodeList of every element matching the selector. Unlike getElementsBy..., this snapshot does not update when the DOM later changes.
const elements = document.querySelectorAll('.example');
elements.forEach((element, index) => {
element.style.backgroundColor = 'lightgreen';
element.textContent = `Element ${index + 1} highlighted!`;
});A NodeList has a real forEach method, so you can iterate it directly. To use map/filter, convert it with Array.from(elements) first.
querySelectorAll returns a static NodeList — it will not reflect elements added or removed afterward. getElementsByClassName and getElementsByTagName return live HTMLCollections that do. Choose static when you want a stable snapshot, live when you want the collection to track the DOM.
Scoping a search to an element
All the querySelector* and getElementsBy* methods also exist on individual elements, not just document. Calling them on an element restricts the search to that element's descendants:
const card = document.querySelector('.card');
const title = card.querySelector('.title'); // only inside .cardScoping keeps queries fast and avoids accidentally matching elements elsewhere on the page.
Testing an element: matches
matches does not return an element — it returns true/false for whether a given element matches a CSS selector. It is ideal inside event handlers and event delegation.
const element = document.getElementById('test');
if (element.matches('.example')) {
element.style.color = 'red';
element.textContent = 'Element matches the selector!';
}Walking up the tree: closest
closest starts at the element itself and walks up through its ancestors, returning the nearest one that matches the selector (or null). It is the cleanest way to find a containing element.
const element = document.getElementById('child');
const parent = element.closest('.outer');
parent.style.border = '2px solid red';matches + closest are the backbone of event delegation: attach one listener to a container, then in the handler use event.target.closest('.item') to figure out which child was actually clicked. Learn more in event handling in the DOM.
Combining selectors for precise targeting
CSS selectors compose, so you can be as specific as you need without extra code:
const element = document.querySelector('.example.special');
element.style.backgroundColor = 'pink';
element.textContent = 'Special element highlighted!';Here .example.special matches an element that has both classes. You can chain combinators (>, descendant space, +, ~), attribute selectors (input[type="email"]), and pseudo-classes (li:first-child).
Which method should I use?
| Method | Returns | Live? | Best for |
|---|---|---|---|
getElementById | single element / null | n/a | one element with a known unique ID |
getElementsByClassName | HTMLCollection | live | all elements of a class, tracking the DOM |
getElementsByTagName | HTMLCollection | live | all elements of a tag |
getElementsByName | NodeList | live | form controls sharing a name |
querySelector | first match / null | n/a | first element matching any CSS selector |
querySelectorAll | NodeList | static | a stable snapshot of all matches |
matches | boolean | n/a | testing if an element fits a selector |
closest | nearest ancestor / null | n/a | finding a containing element |
Rules of thumb:
- Reach for
querySelector/querySelectorAllby default — one consistent API and full CSS power. - Use
getElementByIdwhen you have a unique ID and want the fastest possible lookup. - Use a live collection (
getElementsBy...) only when you specifically want it to track DOM changes; otherwise prefer the staticquerySelectorAll.
Common gotchas
nullresults.getElementById,querySelector, andclosestall returnnullwhen there is no match. Always check before using the result.- Running before the DOM exists. If your script runs in
<head>before the elements are parsed, selections return nothing. Put scripts at the end of<body>, usedefer, or wait forDOMContentLoaded. - Collections aren't arrays.
HTMLCollectionhas noforEach.NodeListhasforEachbut notmap/filter. Convert withArray.from()when in doubt.
Conclusion
Selecting elements is the entry point to every DOM task. For most code, querySelector and querySelectorAll give you one flexible, CSS-powered API; getElementById stays the fastest choice for unique IDs; and matches / closest power clean event delegation. Once you can reliably select elements, continue with DOM manipulation and traversing the DOM.
Performance note: All these methods are fast enough for typical pages. The real cost comes from running them repeatedly in tight loops — cache a selection in a variable instead of re-querying. Static NodeLists from querySelectorAll avoid the reflow bookkeeping that keeps live HTMLCollections up to date.