DOM Selection: getElementById & querySelector
What is the DOM?
The DOM stands for Document Object Model. When your browser loads an HTML page, it does not simply display raw text. Instead, it parses the HTML and creates a tree-like data structure in memory called the DOM. Every HTML element becomes a node in this tree, and each node is a JavaScript object with properties and methods that you can read and modify. The DOM is the bridge between your HTML document and JavaScript -- it is what allows JavaScript to interact with and manipulate the content, structure, and style of a web page.
Think of the DOM as a living representation of your HTML. While the HTML file is static text stored on a server, the DOM is a dynamic object that exists in the browser's memory. When you change the DOM with JavaScript, the browser immediately updates what the user sees on screen. This is the foundation of every interactive web application, from simple dropdown menus to complex single-page applications.
The DOM Tree Structure
The DOM organizes your document as a hierarchical tree. At the very top is the document object, which represents the entire HTML page. Below it sits the <html> element, which branches into <head> and <body>. Every element nested inside another becomes a child node, and every element containing other elements becomes a parent node. Elements at the same nesting level are called sibling nodes.
Example: HTML Source and Its DOM Tree
<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<div id="container">
<h1 class="title">Hello World</h1>
<p class="description">Welcome to my page.</p>
<ul id="nav-list">
<li class="nav-item">Home</li>
<li class="nav-item">About</li>
<li class="nav-item">Contact</li>
</ul>
</div>
</body>
</html>
// DOM Tree Visualization:
// document
// └── html
// ├── head
// │ └── title ("My Page")
// └── body
// └── div#container
// ├── h1.title ("Hello World")
// ├── p.description ("Welcome to my page.")
// └── ul#nav-list
// ├── li.nav-item ("Home")
// ├── li.nav-item ("About")
// └── li.nav-item ("Contact")
<h1> is its own node, separate from the <h1> element node. Comments in HTML also become comment nodes. Understanding these different node types is important for advanced DOM manipulation.The document Object
The document object is the entry point to the DOM. It is a global object available in any JavaScript code running in a browser. Every method you use to find, create, or modify elements starts with document. It provides properties like document.title (the page title), document.body (the body element), document.head (the head element), and document.documentElement (the root <html> element). More importantly, it provides the methods you will use to select elements from the page.
Example: Exploring the document Object
// Access the document title
console.log(document.title); // "My Page"
// Access the body element directly
console.log(document.body); // <body>...</body>
// Access the root html element
console.log(document.documentElement); // <html>...</html>
// Check how many elements are in the body
console.log(document.body.children.length);
// Access all links in the document
console.log(document.links);
// Access all forms in the document
console.log(document.forms);
getElementById -- Selecting by ID
The getElementById() method is the fastest and most straightforward way to select a single element from the DOM. It searches the entire document for an element with the specified id attribute and returns that element as an object. Since IDs must be unique within an HTML document, this method always returns either one element or null if no element with that ID exists.
Example: Using getElementById
// Select the container div
const container = document.getElementById('container');
console.log(container); // <div id="container">...</div>
// Select the navigation list
const navList = document.getElementById('nav-list');
console.log(navList); // <ul id="nav-list">...</ul>
// Try to select a non-existent element
const sidebar = document.getElementById('sidebar');
console.log(sidebar); // null
// Always check if the element exists before using it
const header = document.getElementById('main-header');
if (header) {
console.log('Header found:', header.textContent);
} else {
console.log('Header element not found');
}
# to getElementById(). Unlike CSS selectors, this method takes only the plain ID string. Writing document.getElementById('#container') will return null because no element has an ID of literally "#container". The correct call is document.getElementById('container').getElementsByClassName -- Selecting by Class Name
The getElementsByClassName() method selects all elements that have a specified CSS class. Unlike getElementById() which returns a single element, this method returns an HTMLCollection -- a live, array-like object containing all matching elements. You can pass a single class name or multiple class names separated by spaces, in which case only elements with all specified classes are returned.
Example: Using getElementsByClassName
// Select all elements with class "nav-item"
const navItems = document.getElementsByClassName('nav-item');
console.log(navItems); // HTMLCollection(3) [li.nav-item, li.nav-item, li.nav-item]
console.log(navItems.length); // 3
// Access individual elements by index
console.log(navItems[0].textContent); // "Home"
console.log(navItems[1].textContent); // "About"
console.log(navItems[2].textContent); // "Contact"
// Loop through the collection with a for loop
for (let i = 0; i < navItems.length; i++) {
console.log(navItems[i].textContent);
}
// Select elements with multiple classes
// Only elements that have BOTH classes will be returned
// <div class="card featured"> matches, <div class="card"> does not
const featuredCards = document.getElementsByClassName('card featured');
// You can also call it on a specific parent element
const container = document.getElementById('container');
const itemsInContainer = container.getElementsByClassName('nav-item');
console.log(itemsInContainer.length); // 3
getElementsByTagName -- Selecting by Tag Name
The getElementsByTagName() method selects all elements with the specified HTML tag name. Like getElementsByClassName(), it returns a live HTMLCollection. The tag name is case-insensitive, so 'div', 'DIV', and 'Div' all return the same results. You can also pass '*' to select all elements in the document.
Example: Using getElementsByTagName
// Select all list items on the page
const listItems = document.getElementsByTagName('li');
console.log(listItems.length); // 3
// Select all paragraphs
const paragraphs = document.getElementsByTagName('p');
for (let i = 0; i < paragraphs.length; i++) {
console.log(paragraphs[i].textContent);
}
// Select all elements (useful for counting)
const allElements = document.getElementsByTagName('*');
console.log('Total elements:', allElements.length);
// Select within a specific parent
const navList = document.getElementById('nav-list');
const navListItems = navList.getElementsByTagName('li');
console.log(navListItems.length); // 3 (only li elements inside #nav-list)
querySelector -- CSS Selector Power
The querySelector() method is the most versatile selection method available. It accepts any valid CSS selector string and returns the first element that matches. If no element matches, it returns null. This method brings the full power of CSS selectors into JavaScript -- you can select by ID, class, tag name, attribute, pseudo-class, or any combination of these using the exact same selectors you use in your CSS stylesheets.
Example: Using querySelector with Various Selectors
// Select by ID (equivalent to getElementById)
const container = document.querySelector('#container');
// Select by class (returns the FIRST matching element only)
const firstNavItem = document.querySelector('.nav-item');
console.log(firstNavItem.textContent); // "Home"
// Select by tag name (returns the first matching element)
const firstParagraph = document.querySelector('p');
// Select by attribute
const emailInput = document.querySelector('input[type="email"]');
const dataElement = document.querySelector('[data-role="admin"]');
// Select with descendant combinator
const containerTitle = document.querySelector('#container .title');
// Select with child combinator
const directChild = document.querySelector('#container > h1');
// Select the first list item inside a specific list
const firstItem = document.querySelector('#nav-list li');
// Select using pseudo-classes
const firstChild = document.querySelector('ul li:first-child');
const lastChild = document.querySelector('ul li:last-child');
const thirdChild = document.querySelector('ul li:nth-child(3)');
// Complex selectors work too
const activeLink = document.querySelector('nav a.active[href^="/en"]');
querySelector() is generally preferred over getElementById() because of its flexibility. However, getElementById() is slightly faster in performance since the browser optimizes ID lookups internally. For most applications, the difference is negligible, but in performance-critical code that runs thousands of times per second (like game loops), prefer getElementById() for ID-based selections.querySelectorAll -- Select All Matches
The querySelectorAll() method works like querySelector() but instead of returning only the first match, it returns a NodeList containing all elements that match the CSS selector. If no elements match, it returns an empty NodeList (not null). This method is incredibly powerful for batch operations on groups of elements.
Example: Using querySelectorAll
// Select all navigation items
const allNavItems = document.querySelectorAll('.nav-item');
console.log(allNavItems.length); // 3
// querySelectorAll returns a NodeList which supports forEach
allNavItems.forEach(function(item) {
console.log(item.textContent);
});
// Select all paragraphs inside the container
const containerParagraphs = document.querySelectorAll('#container p');
// Select multiple different elements with comma-separated selectors
const headingsAndParagraphs = document.querySelectorAll('h1, h2, h3, p');
// Select all inputs that are required
const requiredFields = document.querySelectorAll('input[required]');
// Select all even list items using nth-child
const evenItems = document.querySelectorAll('li:nth-child(even)');
// Select all links that open in a new tab
const externalLinks = document.querySelectorAll('a[target="_blank"]');
// Convert NodeList to a real array if you need array methods
const itemsArray = Array.from(allNavItems);
// or using spread operator
const itemsArray2 = [...allNavItems];
NodeList vs HTMLCollection: A Critical Difference
Understanding the difference between NodeList and HTMLCollection is essential for avoiding subtle bugs. These two collection types look similar but behave differently in important ways.
An HTMLCollection is returned by getElementsByClassName() and getElementsByTagName(). It is a live collection, meaning it automatically updates when the DOM changes. If you add or remove elements that match the criteria, the collection's length and contents change in real time. HTMLCollection does not have a forEach() method, so you must use a traditional for loop or convert it to an array.
A NodeList returned by querySelectorAll() is a static collection. It is a snapshot of the DOM at the moment you called the method. Adding or removing elements after the selection does not change the NodeList. NodeList does have a forEach() method, making it easier to iterate over.
Example: Live HTMLCollection vs Static NodeList
// Setup: <ul id="list"><li>A</li><li>B</li></ul>
// HTMLCollection is LIVE
const liveItems = document.getElementsByTagName('li');
console.log(liveItems.length); // 2
// NodeList is STATIC
const staticItems = document.querySelectorAll('li');
console.log(staticItems.length); // 2
// Now add a new list item to the DOM
const newItem = document.createElement('li');
newItem.textContent = 'C';
document.getElementById('list').appendChild(newItem);
// The live HTMLCollection automatically updated!
console.log(liveItems.length); // 3
// The static NodeList did NOT update
console.log(staticItems.length); // 2 (still the original snapshot)
// HTMLCollection does NOT have forEach
// liveItems.forEach(...) would throw an error!
// But you can convert it to an array first
Array.from(liveItems).forEach(function(item) {
console.log(item.textContent);
});
// NodeList DOES have forEach
staticItems.forEach(function(item) {
console.log(item.textContent);
});
Array.from() before modifying the DOM during iteration.Selecting by Attributes
CSS attribute selectors work in querySelector() and querySelectorAll(), giving you fine-grained control over which elements you select. You can match elements by the presence of an attribute, its exact value, or even partial values using various operators. This is especially useful for selecting form elements, data attributes, and ARIA attributes.
Example: Attribute-Based Selection
// Select by presence of an attribute
const elementsWithTitle = document.querySelectorAll('[title]');
// Select by exact attribute value
const submitBtn = document.querySelector('button[type="submit"]');
const checkbox = document.querySelector('input[type="checkbox"]');
// Select by attribute value starting with a string (^=)
const internalLinks = document.querySelectorAll('a[href^="/en"]');
// Select by attribute value ending with a string ($=)
const pdfLinks = document.querySelectorAll('a[href$=".pdf"]');
// Select by attribute value containing a string (*=)
const searchInputs = document.querySelectorAll('input[name*="search"]');
// Select by data attributes
const adminPanels = document.querySelectorAll('[data-role="admin"]');
const activeSlides = document.querySelectorAll('[data-active="true"]');
// Combine attribute selectors with other selectors
const requiredEmailField = document.querySelector(
'form.login input[type="email"][required]'
);
// Select ARIA attributes for accessibility testing
const expandedMenus = document.querySelectorAll('[aria-expanded="true"]');
const hiddenElements = document.querySelectorAll('[aria-hidden="true"]');
closest() -- Searching Up the DOM Tree
While querySelector() searches down through an element's descendants, the closest() method searches up through an element's ancestors. It starts with the element itself and walks up the parent chain, returning the first ancestor that matches the given CSS selector. If no ancestor matches, it returns null. This method is extremely useful in event handling when you need to find a parent container from a clicked child element.
Example: Using closest() to Navigate Up the DOM
// HTML:
// <div class="card" data-id="42">
// <div class="card-body">
// <h3 class="card-title">Product Name</h3>
// <p class="card-text">Description here...</p>
// <button class="btn-delete">Delete</button>
// </div>
// </div>
const deleteBtn = document.querySelector('.btn-delete');
// Find the closest parent with class "card"
const card = deleteBtn.closest('.card');
console.log(card); // <div class="card" data-id="42">...</div>
// Get data from the card
const cardId = card.dataset.id;
console.log(cardId); // "42"
// Find the closest parent with class "card-body"
const cardBody = deleteBtn.closest('.card-body');
console.log(cardBody); // <div class="card-body">...</div>
// closest() checks the element itself first
const selfCheck = card.closest('.card');
console.log(selfCheck === card); // true (it matched itself!)
// Returns null if no ancestor matches
const table = deleteBtn.closest('table');
console.log(table); // null
// Real-world: event delegation pattern
document.addEventListener('click', function(event) {
const card = event.target.closest('.card');
if (card) {
console.log('Clicked inside card:', card.dataset.id);
}
});
matches() -- Testing If an Element Matches a Selector
The matches() method lets you check whether an element matches a given CSS selector. It returns true or false and does not search the DOM -- it simply tests the element you call it on. This is useful in conditional logic, especially when combined with event delegation where you need to verify that a clicked element has a certain class or attribute.
Example: Using matches() for Element Testing
const heading = document.querySelector('h1.title');
// Test if the element matches various selectors
console.log(heading.matches('h1')); // true
console.log(heading.matches('.title')); // true
console.log(heading.matches('h1.title')); // true
console.log(heading.matches('h2')); // false
console.log(heading.matches('.subtitle')); // false
console.log(heading.matches('#container h1')); // true (checks context too)
// Useful in event delegation
document.addEventListener('click', function(event) {
if (event.target.matches('.btn-delete')) {
console.log('Delete button was clicked');
}
if (event.target.matches('a[href^="http"]')) {
console.log('External link was clicked');
}
if (event.target.matches('.nav-item, .nav-link')) {
console.log('Navigation element was clicked');
}
});
Performance of Different Selectors
Not all DOM selection methods perform equally. When you are building complex web applications that frequently query the DOM, understanding relative performance can help you make better choices. Here is a general ranking from fastest to slowest:
- getElementById() -- The fastest because browsers maintain an internal hash map of IDs for instant lookup.
- getElementsByClassName() -- Very fast since class lookups are optimized internally.
- getElementsByTagName() -- Fast because tag names are part of the element's core structure.
- querySelector() -- Slightly slower because the browser must parse the CSS selector string before searching.
- querySelectorAll() -- The slowest because it must find all matches and build a complete NodeList, but the difference is rarely noticeable.
That said, the performance difference between these methods is typically measured in microseconds. For the vast majority of web pages and applications, you should choose whichever method is most readable and appropriate for your use case rather than micro-optimizing selector performance. Readability and maintainability are far more valuable than saving a few microseconds per DOM query.
Example: Performance Comparison
// Measuring selection performance
console.time('getElementById');
for (let i = 0; i < 100000; i++) {
document.getElementById('container');
}
console.timeEnd('getElementById');
// Typically: ~5-15ms for 100,000 lookups
console.time('querySelector by ID');
for (let i = 0; i < 100000; i++) {
document.querySelector('#container');
}
console.timeEnd('querySelector by ID');
// Typically: ~15-40ms for 100,000 lookups
console.time('querySelector complex');
for (let i = 0; i < 100000; i++) {
document.querySelector('#container > .title');
}
console.timeEnd('querySelector complex');
// Typically: ~25-60ms for 100,000 lookups
// The difference per single call is negligible
// Focus on readability over micro-optimization
Caching DOM References
One of the most impactful performance optimizations you can make is caching DOM references. Every time you call a selection method, the browser must search through the DOM tree to find matching elements. If you use the same element multiple times, store the reference in a variable instead of re-querying the DOM each time. This is especially important inside loops, event handlers, and animation callbacks that execute frequently.
Example: Caching DOM References
// BAD: Querying the DOM repeatedly
function updateUI() {
document.querySelector('.status').textContent = 'Loading...';
document.querySelector('.status').style.color = 'orange';
document.querySelector('.status').classList.add('active');
// The browser searches the DOM 3 separate times!
}
// GOOD: Cache the reference in a variable
function updateUI() {
const status = document.querySelector('.status');
status.textContent = 'Loading...';
status.style.color = 'orange';
status.classList.add('active');
// The browser searches the DOM only once!
}
// Cache references at the top of your script for frequently used elements
const elements = {
header: document.getElementById('main-header'),
nav: document.getElementById('main-nav'),
content: document.getElementById('main-content'),
footer: document.getElementById('main-footer'),
sidebar: document.getElementById('sidebar'),
};
// Use cached references throughout your code
elements.header.classList.add('sticky');
elements.sidebar.style.display = 'none';
// Be careful: if the DOM element is removed and re-created,
// your cached reference becomes stale (points to the old element).
// In such cases, re-query the DOM when needed.
Selecting Elements Within a Context
You do not always have to search the entire document. Both querySelector() and querySelectorAll() can be called on any element, not just document. When called on a specific element, they only search within that element's descendants. This narrows the search scope, improves performance, and prevents accidental selection of elements outside your intended context.
Example: Scoped Selection Within a Parent Element
// HTML:
// <div id="section-a">
// <p class="intro">Section A intro</p>
// <p class="detail">Section A detail</p>
// </div>
// <div id="section-b">
// <p class="intro">Section B intro</p>
// <p class="detail">Section B detail</p>
// </div>
// Searching within a specific section
const sectionA = document.getElementById('section-a');
const sectionB = document.getElementById('section-b');
// These only search within their respective sections
const introA = sectionA.querySelector('.intro');
console.log(introA.textContent); // "Section A intro"
const introB = sectionB.querySelector('.intro');
console.log(introB.textContent); // "Section B intro"
// Select all paragraphs only within section A
const parasInA = sectionA.querySelectorAll('p');
console.log(parasInA.length); // 2 (only the ones inside section-a)
// This is very useful for component-based patterns
function initializeCard(cardElement) {
const title = cardElement.querySelector('.card-title');
const body = cardElement.querySelector('.card-body');
const btn = cardElement.querySelector('.card-btn');
// All selections are scoped to this specific card
}
Real-World Example: Building a Tab Navigation System
Let us put all the selection methods together in a practical example. We will build a tab navigation system where clicking a tab button reveals the corresponding content panel. This example demonstrates ID selection, class selection, querySelectorAll, forEach, closest(), matches(), and caching DOM references -- all working together in a realistic scenario.
Example: Tab Navigation with DOM Selection
// HTML Structure:
// <div class="tabs-container" id="product-tabs">
// <div class="tab-buttons">
// <button class="tab-btn active" data-tab="overview">Overview</button>
// <button class="tab-btn" data-tab="specs">Specifications</button>
// <button class="tab-btn" data-tab="reviews">Reviews</button>
// </div>
// <div class="tab-panels">
// <div class="tab-panel active" id="panel-overview">Overview content...</div>
// <div class="tab-panel" id="panel-specs">Specs content...</div>
// <div class="tab-panel" id="panel-reviews">Reviews content...</div>
// </div>
// </div>
// Cache DOM references
const tabContainer = document.getElementById('product-tabs');
const tabButtons = tabContainer.querySelectorAll('.tab-btn');
const tabPanels = tabContainer.querySelectorAll('.tab-panel');
// Function to switch tabs
function switchTab(targetTabName) {
// Deactivate all buttons
tabButtons.forEach(function(btn) {
btn.classList.remove('active');
});
// Deactivate all panels
tabPanels.forEach(function(panel) {
panel.classList.remove('active');
});
// Activate the clicked button
const activeButton = tabContainer.querySelector(
'.tab-btn[data-tab="' + targetTabName + '"]'
);
activeButton.classList.add('active');
// Activate the corresponding panel
const activePanel = document.getElementById('panel-' + targetTabName);
activePanel.classList.add('active');
}
// Use event delegation on the container (best practice)
tabContainer.addEventListener('click', function(event) {
// Check if a tab button was clicked using closest()
const clickedBtn = event.target.closest('.tab-btn');
if (!clickedBtn) return; // Click was not on a tab button
// Get the target tab name from the data attribute
const tabName = clickedBtn.dataset.tab;
switchTab(tabName);
});
Real-World Example: Form Validation with DOM Selection
Another common use case for DOM selection is form validation. This example shows how you can combine multiple selection methods to validate a registration form, selecting specific input fields by type, checking required fields, and displaying error messages by selecting the corresponding error containers.
Example: Form Validation
// Cache form and all required fields
const form = document.getElementById('registration-form');
const requiredFields = form.querySelectorAll('[required]');
const emailField = form.querySelector('input[type="email"]');
const passwordField = form.querySelector('input[type="password"]');
const submitBtn = form.querySelector('button[type="submit"]');
function validateField(field) {
// Find the error message container next to this field
const errorContainer = field.closest('.form-group')
.querySelector('.error-message');
if (!field.value.trim()) {
errorContainer.textContent = 'This field is required';
field.classList.add('error');
return false;
}
// Check email format
if (field.matches('input[type="email"]') &&
!field.value.includes('@')) {
errorContainer.textContent = 'Please enter a valid email';
field.classList.add('error');
return false;
}
// Clear errors
errorContainer.textContent = '';
field.classList.remove('error');
return true;
}
// Validate all required fields on form submission
form.addEventListener('submit', function(event) {
let isValid = true;
requiredFields.forEach(function(field) {
if (!validateField(field)) {
isValid = false;
}
});
if (!isValid) {
event.preventDefault();
// Scroll to the first error
const firstError = form.querySelector('.error');
if (firstError) {
firstError.focus();
}
}
});
Summary of Selection Methods
Here is a quick reference of all the DOM selection methods covered in this lesson:
- document.getElementById(id) -- Returns a single element by its unique ID. The fastest selection method.
- document.getElementsByClassName(className) -- Returns a live HTMLCollection of all elements with the specified class.
- document.getElementsByTagName(tagName) -- Returns a live HTMLCollection of all elements with the specified tag name.
- document.querySelector(selector) -- Returns the first element matching any valid CSS selector.
- document.querySelectorAll(selector) -- Returns a static NodeList of all elements matching any valid CSS selector.
- element.closest(selector) -- Searches up the DOM tree and returns the first ancestor matching the selector.
- element.matches(selector) -- Returns true or false indicating whether the element matches the selector.
Practice Exercise
Create an HTML page with the following structure: a navigation bar with five links (each with a class of nav-link and a data-section attribute), three content sections (each with a unique ID and a class of content-section), and a footer with a paragraph. Then write JavaScript that accomplishes the following tasks: (1) Use getElementById() to select the first content section and log its text. (2) Use getElementsByClassName() to select all navigation links and log their count. (3) Use querySelector() to select the first link with a specific data-section value. (4) Use querySelectorAll() to select all content sections and iterate through them with forEach(). (5) Demonstrate the difference between live HTMLCollection and static NodeList by adding a new element and checking the length of each. (6) Use closest() from a deeply nested element to find its section parent. (7) Cache all frequently used DOM references in an object at the top of your script. Test each method and verify the results in the browser console.