Working with the DOM

Selecting elements, reading and writing content safely, creating nodes, and why innerHTML deserves caution.

Finding elements

document.querySelector('.card');          // first match
const all = document.querySelectorAll('.card'); // static NodeList
all.forEach(el => el.classList.add('seen'));

// cache a reference rather than re-querying in loops
const form = document.querySelector('#signup');

querySelectorAll returns a static NodeList; getElementsByClassName returns a live HTMLCollection that updates as the DOM changes β€” a common source of confusion while looping.

Reading and writing

PropertySets
textContentText only β€” safest choice
innerHTMLParsed HTML β€” powerful, XSS risk
valueForm control's current value
classListAdd/remove/toggle classes
setAttributeAny attribute
el.textContent = userInput;          // rendered as plain text
el.setAttribute('aria-expanded', 'false');
el.dataset.userId = '42';            // data-user-id

// style: prefer classes over inline styles
el.classList.toggle('is-open', open);
el.style.setProperty('--accent', '#4f46e5');
⚠️
Never assign untrusted input to innerHTML β€” el.innerHTML = name is an XSS hole as soon as name contains a script-bearing tag. Use textContent, or sanitize with a trusted library.

Creating and inserting

const li = document.createElement('li');
li.className = 'row';
li.textContent = ' item ';
list.append(li);            // or prepend / before / after

// efficient bulk insert
const frag = document.createDocumentFragment();
items.forEach(i => frag.append(makeRow(i)));
list.append(frag);          // single reflow

li.remove();

Batch DOM writes inside a fragment (or build one HTML string once) rather than appending in a loop β€” every insertion can trigger layout.

Timing your code

  • Put <script> near the end of the body, or use defer so the script runs after parsing.
  • type='module' is deferred by default β€” no need to add defer.
  • async executes as soon as it downloads; order is not guaranteed.
<script src='/app.js' defer></script>
<script type='module' src='/app.js'></script>

FAQ

Why is querySelector returning null?
Your script ran before the element existed. Defer the script, or wait for DOMContentLoaded.
How do I wait for images/fonts too?
window.addEventListener('load', …) fires after all subresources finish.

Events HTML: getting started

Last refreshed 2026-09-17.