The DOM is a tree of objects
The browser parses your HTML into a tree. Every tag becomes an element object, every run of text becomes a text node. JavaScript does not read your file — it reads and changes that tree.
This is why the Elements panel can differ from view-source: the tree has been repaired where the markup was broken, and changed by any script that has run.
querySelector is almost always the answer
document.querySelector(".card"); // the first, or null
document.querySelectorAll("li"); // all of them, as a NodeList
document.querySelector("#main .card:not(.archived) .title");
document.querySelector('[data-id="7"]');
// scoped: search inside one element
card.querySelector(".title");
card.querySelectorAll("button");
document.querySelector(".typo").textContent throws "Cannot read properties of null". That message always means the selector matched nothing.
The older methods
document.getElementById("main"); // fastest, and clear
document.getElementsByClassName("card"); // LIVE HTMLCollection
document.getElementsByTagName("li"); // live too
const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
items[i].classList.remove("item"); // the collection SHRINKS as you go
} // half of them are skipped
querySelectorAll | getElementsBy… | |
|---|---|---|
| Returns | NodeList | HTMLCollection |
| Live? | No — a snapshot | Yes — updates itself |
forEach | Yes | No — spread it first |
| Selector | Any CSS | One class or tag |
Turning a NodeList into an array
[...document.querySelectorAll("li")].map((li) => li.textContent);
Array.from(document.querySelectorAll("li"), (li) => li.textContent);
// NodeList has forEach, but not map, filter or reduce
document.querySelectorAll("li").forEach((li) => { });
Walking the tree
el.parentElement;
el.children; // elements only — an HTMLCollection
el.firstElementChild;
el.lastElementChild;
el.nextElementSibling;
el.previousElementSibling;
// the node versions include text and comments — usually not what you want
el.parentNode; el.childNodes; el.firstChild;
The whitespace between <ul> and <li> is a text node. firstElementChild skips it; firstChild does not.
closest and matches
e.target.closest(".card"); // this element or the nearest ancestor matching
el.matches(".archived"); // true or false
parent.contains(el); // is it inside?
// the delegation idiom
list.addEventListener("click", (e) => {
const row = e.target.closest("[data-id]");
if (!row || !list.contains(row)) return;
open(row.dataset.id);
});
Reading what you found
getAttribute("href") gives you "/learn/"; el.href gives you "https://domvybe.com/learn/". Attribute and property are related but not the same, and for value and checked they diverge as soon as the user types.
el.textContent; // all the text, including hidden elements
el.innerText; // as rendered — slower, triggers layout
el.value; // form controls only
el.id; el.className;
el.classList.contains("open");
el.getAttribute("href"); // the attribute, as written
el.href; // the property — resolved to an absolute URL
el.dataset.panel; // data-panel, always a string
el.getBoundingClientRect(); // position and size, after transforms
el.offsetWidth; // layout width, ignoring transforms
When nothing is found
- Check the spelling of the selector — a missing dot or hash is the usual cause.
- Check when the script runs. A plain
<script>in the head runs before the body exists. Usetype="module"ordefer. - Check whether the element is created later by another script. If so, find it at the moment you need it, not once at the top of the file.
- Log the element itself, not a property of it:
console.log(el)tells younullimmediately.