The DOM: finding things

Find exactly the elements you mean, and know what you got back.

16of 32
≈ 11Min read
8Parts

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

any CSS selector works
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");
querySelector returns null, not an error

document.querySelector(".typo").textContent throws "Cannot read properties of null". That message always means the selector matched nothing.

The older methods

you will meet them, and one is still useful
document.getElementById("main");            // fastest, and clear
document.getElementsByClassName("card");    // LIVE HTMLCollection
document.getElementsByTagName("li");        // live too
why "live" matters
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
querySelectorAllgetElementsBy…
ReturnsNodeListHTMLCollection
Live?No — a snapshotYes — updates itself
forEachYesNo — spread it first
SelectorAny CSSOne class or tag

Turning a NodeList into an array

so the array methods work
[...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

elements, not text nodes
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;
firstChild is usually a text node

The whitespace between <ul> and <li> is a text node. firstElementChild skips it; firstChild does not.

closest and matches

the pair that makes delegation work
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.

text, attributes and data
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. Use type="module" or defer.
  • 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 you null immediately.

Practice