HTML DOM cheat sheet

A scannable HTML DOM reference: 31 short snippets across 13 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Selecting elementsFind nodes with CSS selectors, understand when you get a static list versus a live collection, and scope a search tolesson
Reading and updating content safelytextContent versus innerHTML versus form properties, attribute and class handling, and the script-injection bug thatlesson
Creating, removing and traversingBuild nodes and fragments efficiently, detach and replace them correctly, and walk the tree with the element-onlylesson
Attributes, classes and inline styles from scriptclassList.toggle(name, force) is the version to use in rendering code: given the same state twice it produces the samelesson
Events and delegation in depthThe options object, capture and bubble, delegation with closest, removing listeners reliably, and custom events betweenlesson
Forms, validation and user inputUse requestSubmit(). It is the only programmatic path that behaves like a user pressing the button, and therefore thelesson
Geometry, scrolling and visibilityMeasure elements correctly, understand which size property you want, scroll deliberately, and observe visibility withlesson
Templates, cloning and efficient renderingUse the template element for inert markup, clone it into a fragment, and render lists without rebuilding the whole DOMlesson
Observing changes with MutationObserverThe honest caveat: in your own code, prefer an explicit call at the point of change. An observer is a listener forlesson
Web Components: custom elements and shadow DOMDefine a custom element, use its lifecycle callbacks correctly, encapsulate styles in a shadow root, and compose withlesson
Performance and memory in DOM codeThe DevTools memory panel is the way to confirm this rather than guess: take a heap snapshot, interact for a whilelesson
Accessibility for DOM scriptingThe first rule of ARIA is to use the right element instead. A <button> already has a role, keyboard activationlesson
Debugging and testing DOM codeUse DevTools breakpoints and console utilities to inspect live nodes, then test the same code in JSDOM and in a reallesson

Quick snippets

Selecting elements

querySelector and querySelectorAll

const one  = document.querySelector('#signup .field');    // first match, or null
const many = document.querySelectorAll('ul.todos > li');  // static NodeList

many.forEach(el => el.classList.add('row'));   // a NodeList is iterable
const arr = Array.from(many);                  // convert when you need array methods
const n = document.querySelectorAll('.card').length;

Static lists versus live collections

const live = document.getElementsByClassName('task');   // suppose length is 3
list.append(newTask);
live.length;                    // 4 - the same object changed under you

// snapshot before mutating, or elements get skipped
for (const el of Array.from(live)) el.remove();

// same query, same result - only the liveness differs
document.querySelectorAll('.task').length;   // 3

Scoping a search and walking upwards

// search inside one subtree instead of the whole document
const card = document.querySelector('.card');
card.querySelector('.title');          // only nodes inside this card
card.querySelectorAll('a[href]');      // relative to the card

// walk up from the element you already have
const row  = event.target.closest('tr[data-id]');
const form = input.closest('form');
const outsideModal = el.closest('.modal') === null;

// test a single element without searching for it
if (link.matches('a[target="_blank"]')) { /* ... */ }

Full lesson: Selecting elements →

Reading and updating content safely

Choosing the right property

title.textContent = user.name;            // safe: rendered as plain text
badge.dataset.state = 'ready';            // writes <span data-state="ready">
output.value = String(total);             // form controls use value, not textContent
note.insertAdjacentText('beforeend', ' ok');

// reading back
badge.dataset.state;                      // 'ready'
select.options[select.selectedIndex].text;

Attributes, classes and inline styles

link.setAttribute('href', '/docs');
link.getAttribute('aria-expanded');     // null when the attribute is absent
link.hasAttribute('hidden');
link.removeAttribute('hidden');

// toggle takes a second argument that forces the state
panel.classList.toggle('is-open', isOpen);
panel.classList.replace('old', 'new');
panel.classList.contains('is-open');

// styles: prefer classes, use custom properties for values computed at runtime
panel.style.setProperty('--hue', hue + 'deg');

Running your code at the right time

function boot() {
  const list = document.querySelector('#todos');   // null if this ran too early
  if (!list) return;
  list.addEventListener('click', onClick);
}

if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', boot);   // HTML parsed
} else {
  boot();          // already parsed: the script was deferred or loaded late
}

Full lesson: Reading and updating content safely →

Creating, removing and traversing

Creating and inserting

const li = document.createElement('li');
li.className = 'task';
li.textContent = label;

// build in a fragment, insert once
const frag = document.createDocumentFragment();
items.forEach(item => frag.append(makeRow(item)));
list.append(frag);          // one insertion, one style recalculation

list.prepend(li);                    // first child
divider.before(li);                  // sibling-level insert
ref.insertAdjacentElement('beforebegin', node);

Removing, replacing and cloning

node.remove();                            // detach from the tree
oldNode.replaceWith(newNode);
parent.replaceChildren();                 // empty it fast

// cloning copies attributes, text and children - not listeners, not form state
const fresh = template.cloneNode(true);
fresh.querySelector('input').value = '';  // reset what the clone inherited

Traversing the tree

el.parentElement;             // null at <html>
el.children;                  // elements only (live)
el.childNodes;                // includes text and comment nodes
el.firstElementChild;
el.lastElementChild;
el.nextElementSibling;
el.previousElementSibling;

// filter one level without walking whitespace nodes
const done = [...list.children].filter(li => li.matches('.done'));

Full lesson: Creating, removing and traversing →

Attributes, classes and inline styles from script

Attributes and their properties

// boolean attributes: presence is the value, not the string
const btn = document.querySelector('button');
btn.setAttribute('disabled', 'false');   // still disabled - presence is enough
btn.disabled = false;                    // this is what you meant

btn.removeAttribute('disabled');         // the other correct form

// copy every attribute from one node to another
for (const { name, value } of [...source.attributes]) {
  target.setAttribute(name, value);
}

Dataset and classList

<div id="row"
     data-user-id="42"
     data-role="admin"
     data-gdpr-consent="2026-01-04"></div>

Dataset and classList

const row = document.querySelector('#row');

row.dataset.userId;       // "42"      data-user-id
row.dataset.role;         // "admin"
row.dataset.gdprConsent;  // "2026-01-04"

row.dataset.userId = '43';            // writes back to the attribute
delete row.dataset.role;              // removes data-role

row.getAttribute('data-user-id');     // still works, same storage

Full lesson: Attributes, classes and inline styles from script →

Events and delegation in depth

Target, phases and propagation

// some events do not bubble by default
el.addEventListener('focus', fn);      // does not bubble
el.addEventListener('focusin', fn);    // does bubble - use this for delegation
el.addEventListener('mouseenter', fn); // does not bubble, use mouseover
el.addEventListener('change', fn);     // bubbles (unlike input on some controls)

Full lesson: Events and delegation in depth →

Forms, validation and user input

The constraint validation API

<form id="signup" novalidate>
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required
         minlength="6" autocomplete="email">
  <span class="error" aria-live="polite"></span>

  <label for="age">Age</label>
  <input id="age" name="age" type="number" min="18" max="120">

  <button type="submit">Sign up</button>
</form>

Full lesson: Forms, validation and user input →

Geometry, scrolling and visibility

IntersectionObserver and ResizeObserver

// react to size changes without a resize listener
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    entry.target.dataset.size = width < 400 ? 'narrow' : 'wide';
  }
});
ro.observe(document.querySelector('.panel'));

IntersectionObserver and ResizeObserver

// distinguishing "never appeared" from "scrolled past"
const sentinel = document.querySelector('#top');
new IntersectionObserver(([entry]) => {
  toolbar.classList.toggle('is-stuck', !entry.isIntersecting);
}, { threshold: 0 }).observe(sentinel);

Full lesson: Geometry, scrolling and visibility →

Templates, cloning and efficient rendering

The template element

<template id="row-template">
  <li class="row">
    <img class="row__avatar" alt="" width="32" height="32">
    <span class="row__name"></span>
    <button class="row__remove" type="button">Remove</button>
  </li>
</template>

The template element

const tpl = document.querySelector('#row-template');
tpl.content;                    // a DocumentFragment - the real markup store
tpl.content.childNodes.length;  // 1

// clone, fill, insert
const node = tpl.content.cloneNode(true);         // deep
const li = node.querySelector('.row');
li.dataset.id = user.id;
li.querySelector('.row__name').textContent = user.name;
li.querySelector('.row__avatar').src = user.avatar;

list.append(node);

Batching with a fragment

// the expensive alternative: one insert per row, each triggering work
for (const user of users) {
  list.append(makeRow(user));       // 500 inserts, 500 layout invalidations
}

// and the worst version: rebuilding innerHTML in a loop
list.innerHTML = '';
users.forEach((u) => { list.innerHTML += makeRowHtml(u); });   // reparses everything

Full lesson: Templates, cloning and efficient rendering →

Observing changes with MutationObserver

Breaking the loop

// disconnecting and reconnecting around a bulk change
observer.disconnect();
container.replaceChildren(...buildAll());
observer.observe(container, { childList: true, subtree: true });

// or drain the queue without a callback
const pending = observer.takeRecords();
observer.disconnect();

What it is genuinely for

// enhance markup that third-party code injects
const enhancer = new MutationObserver((records) => {
  for (const record of records) {
    for (const node of record.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;
      node.querySelectorAll('[data-tooltip]').forEach(attachTooltip);
      if (node.matches('[data-tooltip]')) attachTooltip(node);
    }
  }
});
enhancer.observe(document.querySelector('#chat'), { childList: true, subtree: true });

What it is genuinely for

// debounced save driven by DOM changes
let timer;
const saver = new MutationObserver(() => {
  clearTimeout(timer);
  timer = setTimeout(() => save(editor.innerHTML), 500);
});
saver.observe(editor, { childList: true, subtree: true, characterData: true });

Full lesson: Observing changes with MutationObserver →

Web Components: custom elements and shadow DOM

Defining a custom element

<rating-stars value="3"></rating-stars>

Shadow root, styles and slots

<template id="card">
  <style>
    :host { display: block; border: 1px solid var(--border, #ddd); }
    :host([disabled]) { opacity: 0.5; pointer-events: none; }
    .title { font-weight: 600; }
    ::slotted(img) { border-radius: 6px; }
  </style>
  <div class="title"><slot name="title">Untitled</slot></div>
  <div class="body"><slot></slot></div>
</template>

Shadow root, styles and slots

class SummaryCard extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.append(document.querySelector('#card').content.cloneNode(true));
  }

  get icons() {
    return this.shadowRoot.querySelector('slot[name="icon"]').assignedElements();
  }
}

Full lesson: Web Components: custom elements and shadow DOM →

Performance and memory in DOM code

Animation frames and scheduling

// let the browser finish a long task before inserting 500 rows
function insertInChunks(parent, nodes, chunk = 50) {
  let i = 0;
  function step() {
    const fragment = document.createDocumentFragment();
    for (let n = 0; n < chunk && i < nodes.length; n++, i++) fragment.append(nodes[i]);
    parent.append(fragment);
    if (i < nodes.length) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

Full lesson: Performance and memory in DOM code →

Accessibility for DOM scripting

Live regions and keyboard interaction

<p id="status" role="status" aria-live="polite"></p>
<p id="errors" role="alert" aria-live="assertive"></p>
<div id="results" aria-live="polite" aria-atomic="false"></div>

Live regions and keyboard interaction

// announce a change: the region must exist before you write to it
status.textContent = 'Saved 3 changes';
status.textContent = 'Saved 4 changes';   // a different string, or nothing is announced

// for a list, announce a summary rather than 500 items
results.setAttribute('aria-live', 'polite');
results.textContent = 'Showing 20 of 340 results';

Full lesson: Accessibility for DOM scripting →

Debugging and testing DOM code

DevTools for DOM work

// a DOM breakpoint: right-click a node in Elements
//   Break on -> subtree modifications
//   Break on -> attribute modifications
//   Break on -> node removal

// the debugger then pauses at the exact line that changed the node,
// with the call stack that caused it - the fastest way to find
// the code responsible for an unexpected DOM change

JSDOM and unit tests

// vitest.config.ts
export default defineConfig({
  test: {
    environment: "jsdom",
    setupFiles: ["./test/setup.ts"],
  },
});

Real-browser tests

// an accessible-name check is a cheap regression guard
await expect(page.getByRole("button", { name: "Close dialog" })).toBeVisible();
await expect(page.getByRole("heading", { level: 2 })).toHaveText("Invite a teammate");

Full lesson: Debugging and testing DOM code →

FAQ

Is this HTML DOM cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 13 lessons of the HTML DOM course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full HTML DOM course — it carries the worked explanations, the edge cases and the exercises behind every line here.

HTML CSS JavaScript TypeScript AJAX JSON

Last refreshed 2026-09-27.