JavaScript DOM Manipulation Cheat Sheet
Quick-reference for vanilla JavaScript DOM APIs. Each section includes copy-ready snippets with inline output comments.
Selecting Elements
querySelector and querySelectorAll use CSS selectors for all element selection. Always guard against null.
const btn = document.querySelector('.submit-btn');
const header = document.querySelector('#main-header');
const input = document.querySelector('input[type="email"]');const items = document.querySelectorAll('.list-item');
items.length // number of matches
items.forEach(item => console.log(item.textContent));querySelectorAll returns a static NodeList (snapshot). Use Array.from() or spread to get a real array with map/filter.
const nav = document.querySelector('nav');
const links = nav?.querySelectorAll('a');
// Searches only within <nav>, not the whole documentconst el = document.querySelector('#panel');
if (!el) return; // element might not exist yet
// Or with optional chaining
document.querySelector('#toggle')?.addEventListener('click', handler);Creating and Inserting Elements
Modern insertion methods (append, prepend, before, after) are cleaner than the legacy appendChild/insertBefore.
const li = document.createElement('li');
li.textContent = 'New item';
li.classList.add('list-item');
li.dataset.id = '42';const parent = document.querySelector('ul');
parent.append(li); // as last child
parent.prepend(li); // as first child
sibling.before(li); // before sibling
sibling.after(li); // after siblingconst list = document.querySelector('ul');
list.insertAdjacentHTML('beforeend', '<li>New item</li>');
// Positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'Only use insertAdjacentHTML with trusted content. For user input, create elements with textContent instead.
const item = document.querySelector('.remove-me');
item?.remove(); // removes from DOM
// Remove all children
const container = document.querySelector('#list');
container.replaceChildren(); // empties containerclassList API
Manage CSS classes without string manipulation. Cleaner and safer than modifying className directly.
const el = document.querySelector('#panel');
el.classList.add('active', 'visible'); // add one or more
el.classList.remove('hidden'); // remove
el.classList.toggle('expanded'); // add/remove
el.classList.toggle('dark', isDarkMode); // force based on booleanel.classList.contains('active') // => true/false
el.classList.replace('old', 'new') // swap in one stepfunction switchTab(activeTab) {
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab === activeTab);
});
}Dataset Attributes
Access data-* attributes via the dataset property. Attribute names are auto-converted from kebab-case to camelCase.
// HTML: <div data-user-id="42" data-role="admin"></div>
const el = document.querySelector('[data-user-id]');
el.dataset.userId // => '42' (kebab -> camelCase)
el.dataset.role // => 'admin'
el.dataset.status = 'active';
// HTML becomes: <div ... data-status="active">list.addEventListener('click', (e) => {
const item = e.target.closest('[data-id]');
if (!item) return;
const id = item.dataset.id;
handleClick(id);
});document.querySelector('[data-active="true"]');
document.querySelectorAll('[data-category="fruit"]');Event Listeners
addEventListener is the standard way to attach event handlers. Use removeEventListener or AbortController to clean up.
// Recommended: addEventListener (multiple handlers OK)
btn.addEventListener('click', handleClick);
btn.addEventListener('click', logClick);
// Old style: onclick (overwrites previous handler)
btn.onclick = handleClick; // logClick lost!element.addEventListener('click', (e) => {
e.target // element that triggered the event
e.currentTarget // element the listener is on
e.preventDefault() // stop default behavior
e.stopPropagation() // stop bubbling to parent
});function handler(e) { /* ... */ }
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);
// Or use AbortController
const controller = new AbortController();
btn.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // removes the listenerbtn.addEventListener('click', handler, { once: true });
// Fires once, then auto-removes
window.addEventListener('scroll', onScroll, { passive: true });
// Cannot call preventDefault() — browser can optimize scrollingEvent Delegation
Attach one listener on a parent instead of per-item listeners. Uses event bubbling and closest() for targeting.
const list = document.querySelector('.items');
list.addEventListener('click', (e) => {
const item = e.target.closest('li[data-id]');
if (!item) return; // clicked outside any item
console.log('Clicked item:', item.dataset.id);
});// BAD: one listener per item (breaks for dynamic items)
document.querySelectorAll('li').forEach(li =>
li.addEventListener('click', handler)
);
// GOOD: one listener handles all items (even new ones)
document.querySelector('ul').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) handler(li);
});Delegation works because events bubble from target to ancestors. closest() finds the element you care about regardless of which child was clicked.
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn) return;
const { action } = btn.dataset;
const id = btn.closest('li').dataset.id;
if (action === 'edit') editItem(id);
if (action === 'delete') deleteItem(id);
});Content: innerHTML vs textContent
textContent is safe for untrusted input. innerHTML parses HTML and can introduce XSS vulnerabilities.
const output = document.querySelector('#output');
const userInput = '<script>alert("xss")</script>';
output.textContent = userInput;
// Renders as visible text, NOT executed HTMLconst container = document.querySelector('#content');
container.innerHTML = '<h2>Title</h2><p>Paragraph</p>';
// Parsed and rendered as HTML
// WARNING: Never do this with user input!
// container.innerHTML = userInput; // XSS vulnerability// Before: child elements have event listeners
container.innerHTML = ''; // Wipes all children
// All event listeners on those children are GONE
// Safer alternative: use replaceChildren()
container.replaceChildren();Setting innerHTML destroys child nodes and their listeners. Use event delegation on a surviving parent, or use textContent/insertAdjacentHTML.
Element Position and Size
Use getBoundingClientRect for viewport-relative position, and offset/client properties for dimensions.
const rect = element.getBoundingClientRect();
rect.top // distance from viewport top
rect.left // distance from viewport left
rect.width // element width (padding + border)
rect.height // element height
rect.bottom // top + height
rect.right // left + widthelement.offsetWidth // width + padding + border
element.offsetHeight // height + padding + border
element.clientWidth // width + padding (no border)
element.clientHeight // height + padding (no border)element.scrollTop // pixels scrolled from top
element.scrollLeft // pixels scrolled from left
element.scrollHeight // total scrollable height
// Scroll into view
element.scrollIntoView({ behavior: 'smooth', block: 'center' });const styles = getComputedStyle(element);
styles.color // resolved color value
styles.fontSize // '16px'
styles.display // 'block', 'flex', etc.
// element.style only reads INLINE styles, not CSS
element.style.color // '' if set via stylesheetMutationObserver
Watch for DOM changes asynchronously. Useful for responding to third-party code, lazy-loaded content, or framework-managed DOM.
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Children changed:', mutation.addedNodes);
}
}
});
observer.observe(document.querySelector('#list'), {
childList: true,
});observer.observe(element, {
attributes: true,
attributeFilter: ['class', 'data-state'],
});observer.observe(document.body, {
childList: true,
subtree: true, // observe all descendants
});
// Stop observing
observer.disconnect();MutationObserver is asynchronous (microtask). It batches changes and fires the callback after the current task completes.
Form Element Access
Access form values through FormData, element properties, or the named forms collection.
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(form);
const email = data.get('email');
const values = Object.fromEntries(data);
});const input = document.querySelector('#email');
input.value // current value
input.checked // for checkboxes/radios
input.selectedIndex // for <select>
// Set value programmatically
input.value = 'new@email.com';const input = document.querySelector('input[required]');
input.checkValidity() // => true/false
input.validity.valueMissing // => true if empty
input.setCustomValidity('Custom error message');
input.reportValidity(); // show validation UIdocument.addEventListener('DOMContentLoaded', () => {
// Safe to query elements here
const form = document.querySelector('form');
// ...
});
// Or use defer on script tag:
// <script src="app.js" defer></script>Scripts without defer run before the DOM is ready. Use DOMContentLoaded or the defer attribute to ensure elements exist.
Batch Insertion: DocumentFragment
Build a tree of nodes off-DOM, then insert in one operation. Reduces reflows for bulk insertions.
const fragment = document.createDocumentFragment();
const items = ['Apple', 'Banana', 'Cherry'];
items.forEach(text => {
const li = document.createElement('li');
li.textContent = text;
fragment.append(li); // no reflow yet
});
document.querySelector('ul').append(fragment);
// Single reflow for all itemsconst template = document.querySelector('#item-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.name').textContent = 'Alice';
document.querySelector('#list').append(clone);HTML <template> content is inert (not rendered, scripts not executed) until cloned and inserted.
Can you write this from memory?
Select the element with id "main-title".