Selecting and Changing Elements
querySelector, classList, dataset and the methods that actually get used. The small set of DOM operations that covers most day to day work.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Selecting
document.querySelector(".card"); // the first match, or null
document.querySelectorAll(".card"); // a static NodeList of all matches
document.getElementById("chart"); // by id, fastest
document.getElementsByClassName("card");// a live HTMLCollection
document.getElementsByTagName("p"); // a live HTMLCollectionIn practice querySelector and querySelectorAll cover almost everything: any CSS selector works, and the API is consistent.
Static versus live
const staticList = document.querySelectorAll(".card"); // a snapshot
const liveList = document.getElementsByClassName("card"); // updates itself
document.body.append(newCard);
staticList.length; // unchanged
liveList.length; // one moreA live collection updating while you iterate over it is a classic infinite loop. Prefer the static querySelectorAll.
Scoped selection
const card = document.querySelector(".card");
const title = card.querySelector("h3"); // searches inside card onlyAlways scope a search to the smallest container that makes sense. It is faster and it stops a selector accidentally matching something elsewhere on the page.
Iterating
document.querySelectorAll(".card").forEach((card) => {
card.classList.add("is-ready");
});
// a NodeList is not an array; convert when array methods are needed
const titles = [...document.querySelectorAll("h2")].map((h) => h.textContent);Changing content
el.textContent = "Design course"; // safe, fast
el.innerHTML = "<strong>Design</strong>"; // parses markup - only with trusted valuesChanging classes
el.classList.add("is-open");
el.classList.remove("is-open");
el.classList.toggle("is-open");
el.classList.toggle("is-open", shouldBeOpen); // force a state
el.classList.contains("is-open");
el.classList.replace("is-loading", "is-ready");Never assign to className to add one class - it replaces every class on the element.
Changing attributes
link.href = "/courses/design";
img.src = "/images/design.jpg";
img.alt = "Students at drawing boards";
input.value = "Meera";
input.disabled = true;
button.setAttribute("aria-expanded", "true");
el.removeAttribute("hidden");
el.hidden = true; // the property form
el.dataset.status = "paid";Common attributes have direct properties. ARIA attributes generally do not, so setAttribute is used for those.
Changing styles
// avoid: hard to override, mixes design into script
el.style.backgroundColor = "#1e3a8a";
// prefer: the stylesheet keeps the design
el.classList.add("is-highlighted");
// for genuinely dynamic values, a custom property
el.style.setProperty("--progress", "68%");Note that el.style reads only inline styles. To read a computed value:
getComputedStyle(el).backgroundColor;Showing and hiding
el.hidden = true; // simplest, removes it from the accessibility tree
el.style.display = "none";
el.classList.add("is-hidden");| Method | Visible | In the tab order | Announced | Takes space |
|---|---|---|---|---|
hidden / display: none | No | No | No | No |
visibility: hidden | No | No | No | Yes |
opacity: 0 | No | Yes | Yes | Yes |
.visually-hidden | No | Yes | Yes | No |
aria-hidden="true" | Yes | Yes | No | Yes |
Two rows are traps. opacity: 0 leaves the element fully focusable, so a keyboard user tabs into something invisible. And aria-hidden="true" on a focusable element creates a control that can be reached but not announced - the worst of both.
.visually-hidden is the deliberate case: hidden from sight, present for screen readers. That is how skip links and off screen labels work.
Building elements
const card = document.createElement("article");
card.className = "card";
card.dataset.id = course.id;
const title = document.createElement("h3");
title.textContent = course.title;
const link = document.createElement("a");
link.href = `/courses/${encodeURIComponent(course.slug)}`;
link.textContent = "Read more";
card.append(title, link); // several at once
container.append(card);For anything more complex than this, use a template element instead of building node by node.
A worked example
<div class="filters">
<button type="button" data-filter="all" aria-pressed="true">All</button>
<button type="button" data-filter="design" aria-pressed="false">Design</button>
</div>
<ul id="courses">
<li data-category="design">Product design</li>
<li data-category="data">Analytics</li>
</ul>
<p id="count" aria-live="polite"></p>const filters = document.querySelector(".filters");
const items = [...document.querySelectorAll("#courses li")];
const count = document.getElementById("count");
function applyFilter(value) {
let shown = 0;
items.forEach((item) => {
const show = value === "all" || item.dataset.category === value;
item.hidden = !show;
if (show) shown += 1;
});
filters.querySelectorAll("button").forEach((button) => {
button.setAttribute("aria-pressed", String(button.dataset.filter === value));
});
count.textContent = `Showing ${shown} of ${items.length} courses`;
}
filters.addEventListener("click", (event) => {
const button = event.target.closest("button[data-filter]");
if (button) applyFilter(button.dataset.filter);
});Three details worth copying: hidden rather than a CSS class, so filtered items leave the tab order; aria-pressed kept accurate so the active filter is announced; and a live region so the result count is spoken without moving focus.
Important rules
querySelectorreturnsnullwhen nothing matches. Check before using it.querySelectorAllreturns a static list;getElementsBy*return live ones.- A NodeList has
forEachbut notmaporfilter. Spread it first. - Assigning to
classNamereplaces every class. el.stylereads inline styles only.- Selecting is cheap; changing layout is not.
Common mistakes
- Not checking for
nulland getting an error on a page where the element does not exist. - Using
classNameto add a class. - Modifying a live collection while iterating it.
- Setting styles from script instead of toggling a class.
- Hiding with
opacity: 0and leaving focusable elements behind. - Querying the whole document when a container would do.
- Building large structures with
innerHTMLstrings.
Best practices
querySelectorandquerySelectorAllfor nearly everything.- Scope queries to a container.
- Cache selections rather than querying repeatedly.
- Toggle classes and data attributes; keep styles in CSS.
- Use
hiddento hide things properly. - Keep ARIA state attributes in step with what the reader sees.
- Use
templatefor repeated structures.
Practice
- Build a filter that hides items with
hiddenand keepsaria-pressedaccurate. - Hide an element four different ways and tab through each. Which ones can you still reach?
- Take a live
getElementsByClassNamecollection, add a matching element, and observe the length change. - Replace a chain of
el.styleassignments with a single class toggle.