1Selecting DOM Elements
▶
Before you can read or change anything on a web page using JavaScript, you first need a way to find the elements you care about. That process — locating a specific element or group of elements in the page's structure — is called DOM selection. The Document Object Model (DOM) is the browser's live, in-memory representation of your HTML document, organized as a tree of nodes. Every tag you write in HTML becomes a node in that tree, and JavaScript can navigate and manipulate that tree at any time while the page is open.
Selection always begins with the global document object. The document object is automatically available in any script running in a browser; it represents the entire HTML page and acts as the entry point to every element within it. You call selection methods on document (or sometimes on another element, as you'll see later), passing a description of what you want, and the browser searches its tree and hands back a reference.
That reference is a live JavaScript object — a full-featured instance of an interface like HTMLDivElement or HTMLInputElement — whose properties you can read and whose content you can change. The critical thing to understand upfront is that selecting an element does not modify it. Selection is purely a lookup: you get a reference, and then you decide what (if anything) to do with it. This separation between finding an element and changing it keeps your code predictable and easy to reason about.
JavaScript provides several methods for DOM selection, each suited to different situations. The three you will use most frequently are getElementById, querySelector, and querySelectorAll.
getElementById is the oldest and most specialized of the three. Its syntax is straightforward: you call it on document and pass the element's id attribute value as a plain string — without a # prefix, because you are not writing a CSS selector here, just a raw identifier value.
// HTML: <section id="hero">Welcome</section>
const heroSection = document.getElementById('hero');
console.log(heroSection); // <section id="hero">Welcome</section>
If an element with that id exists in the document, you get back the corresponding Element object. If nothing matches, you get null. Because HTML requires id values to be unique per page, getElementById will always return at most one element. More importantly, browsers maintain an internal index of id values, which makes this method the fastest DOM selection method available. For elements you access repeatedly — a navigation bar, a modal dialog, a form — storing the result in a variable is a common and efficient pattern.
const modal = document.getElementById('confirmModal');
if (modal !== null) {
modal.style.display = 'block'; // safe to use — element exists
}
querySelector is more versatile. Its argument is a full CSS selector string, written exactly as you would write it in a stylesheet — complete with # for ids, . for classes, tag names, attribute selectors, combinators, and pseudo-classes.
// By id
const nav = document.querySelector('#mainNav');
// By class
const firstCard = document.querySelector('.card');
// By tag
const firstParagraph = document.querySelector('p');
// Attribute selector
const emailInput = document.querySelector('input[type="email"]');
// Descendant combinator
const submitBtn = document.querySelector('form .btn-submit');
The key behavior to internalize: querySelector returns only the first matching element in document order (top to bottom, left to right through the source). Even if a hundred elements share the same class, you get back exactly one — the first one the browser encounters while scanning the tree. If nothing matches, the return value is null, just like getElementById. Always guard against null before accessing properties on the result.
const highlight = document.querySelector('.highlight');
if (highlight) {
highlight.classList.add('active'); // 'if (highlight)' is falsy-check for null
}
Because querySelector accepts any valid CSS selector, it can replace getElementById entirely (using the #id syntax). In practice, teams often use getElementById for id-based lookups where speed matters most, and querySelector for everything more complex.
querySelectorAll works like querySelector but instead of stopping at the first match, it collects every element in the document that matches the selector and returns them together as a NodeList.
// Select every list item inside a navigation element
const navItems = document.querySelectorAll('nav li');
console.log(navItems.length); // e.g., 5
// Select all elements carrying the 'card' class
const cards = document.querySelectorAll('.card');
There are two important characteristics of this NodeList to remember. First, it is static: it reflects the state of the DOM at the exact moment the call was made. If you subsequently add or remove elements from the page, the NodeList you already have does not update. Second, if nothing matches the selector, querySelectorAll returns an empty NodeList — not null. This means you never need to null-check the return value; iterating over an empty NodeList is simply a no-op.
const items = document.querySelectorAll('.menu-item');
// forEach — built into NodeList
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// for...of — works with any iterable
for (const item of items) {
console.log(item.textContent);
}
// Spread to a true array if you need array methods like .map or .filter
const itemArray = [...items];
const texts = itemArray.map(el => el.textContent.trim());
The table below summarizes the key differences between the three methods at a glance:
| Method | Argument type | Returns | When nothing matches | Can be scoped? |
|---|---|---|---|---|
getElementById |
Raw id string (no #) |
Single Element |
null |
No — document only |
querySelector |
Any CSS selector | First matching Element |
null |
Yes |
querySelectorAll |
Any CSS selector | Static NodeList |
Empty NodeList |
Yes |
Scoped selection is a powerful technique that becomes especially useful as pages grow in complexity. Both querySelector and querySelectorAll can be called not just on document, but on any element reference you already hold. When you do that, the browser limits its search to that element's descendants only — it does not scan the rest of the page.
const sidebar = document.querySelector('#sidebar');
// Only finds .widget elements that are inside #sidebar
const sidebarWidgets = sidebar.querySelectorAll('.widget');
// Only finds the first <a> inside #sidebar
const firstLink = sidebar.querySelector('a');
Scoped selection has two advantages. Performance-wise, the browser searches a smaller subtree rather than the entire document. Clarity-wise, the code explicitly communicates that you expect these elements to live inside a particular container — a valuable signal for anyone reading your code later. Note that getElementById is defined only on the document object and cannot be called on an element reference; it is the one selection method that cannot be scoped.
Reading and verifying selection results is a habit every JavaScript developer must build. Both getElementById and querySelector return null when no match is found, and trying to access a property on null throws a TypeError that can crash your script.
// Dangerous — will throw if '#nonexistent' does not exist
document.querySelector('#nonexistent').textContent = 'Hello';
// Safe pattern 1 — explicit null check
const el = document.querySelector('#nonexistent');
if (el !== null) {
el.textContent = 'Hello';
}
// Safe pattern 2 — optional chaining (modern JavaScript)
document.querySelector('#nonexistent')?.classList.add('visible');
For querySelectorAll, because the return value is always a NodeList (never null), no null-check is needed. You can loop over the result unconditionally; an empty NodeList simply means the loop body never runs.
One of the most practical tools for learning and debugging selection is the browser's built-in DevTools console. Open it with F12 (or right-click → Inspect), click the Console tab, and type any selection expression directly. The browser evaluates it immediately against the live page and displays the result — letting you confirm that your selector syntax is correct before writing it into your script. For example, typing document.querySelectorAll('.card') shows you exactly how many cards were found and lets you expand each one to inspect its properties.
// Try these directly in the DevTools console on any page:
document.querySelector('h1');
document.querySelectorAll('a[href^="https"]');
document.getElementById('footer');
Mastering these three selection methods — understanding what each accepts, what each returns, and how to handle edge cases — forms the foundation of all DOM manipulation. Every animation, form validation, dynamic content update, and interactive widget you build in vanilla JavaScript will begin with one of these calls to locate the element you need to work with.