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
| Topic | What it covers | |
|---|---|---|
| Selecting elements | Find nodes with CSS selectors, understand when you get a static list versus a live collection, and scope a search to | lesson |
| Reading and updating content safely | textContent versus innerHTML versus form properties, attribute and class handling, and the script-injection bug that | lesson |
| Creating, removing and traversing | Build nodes and fragments efficiently, detach and replace them correctly, and walk the tree with the element-only | lesson |
| Attributes, classes and inline styles from script | classList.toggle(name, force) is the version to use in rendering code: given the same state twice it produces the same | lesson |
| Events and delegation in depth | The options object, capture and bubble, delegation with closest, removing listeners reliably, and custom events between | lesson |
| Forms, validation and user input | Use requestSubmit(). It is the only programmatic path that behaves like a user pressing the button, and therefore the | lesson |
| Geometry, scrolling and visibility | Measure elements correctly, understand which size property you want, scroll deliberately, and observe visibility with | lesson |
| Templates, cloning and efficient rendering | Use the template element for inert markup, clone it into a fragment, and render lists without rebuilding the whole DOM | lesson |
| Observing changes with MutationObserver | The honest caveat: in your own code, prefer an explicit call at the point of change. An observer is a listener for | lesson |
| Web Components: custom elements and shadow DOM | Define a custom element, use its lifecycle callbacks correctly, encapsulate styles in a shadow root, and compose with | lesson |
| Performance and memory in DOM code | The DevTools memory panel is the way to confirm this rather than guess: take a heap snapshot, interact for a while | lesson |
| Accessibility for DOM scripting | The first rule of ARIA is to use the right element instead. A <button> already has a role, keyboard activation | lesson |
| Debugging and testing DOM code | Use DevTools breakpoints and console utilities to inspect live nodes, then test the same code in JSDOM and in a real | lesson |
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 storageFull 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 everythingFull 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?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
HTML CSS JavaScript TypeScript AJAX JSON
Last refreshed 2026-09-27.