Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. JavaScript
  4. JavaScript DOM Manipulation Cheat Sheet
JavaScriptCheat Sheet

JavaScript DOM Manipulation Cheat Sheet

Quick-reference for vanilla JavaScript DOM APIs. Each section includes copy-ready snippets with inline output comments.

On this page
  1. 1Selecting Elements
  2. 2Creating and Inserting Elements
  3. 3classList API
  4. 4Dataset Attributes
  5. 5Event Listeners
  6. 6Event Delegation
  7. 7Content: innerHTML vs textContent
  8. 8Element Position and Size
  9. 9MutationObserver
  10. 10Form Element Access
  11. 11Batch Insertion: DocumentFragment
Selecting ElementsCreating and Inserting ElementsclassList APIDataset AttributesEvent ListenersEvent DelegationContent: innerHTML vs textContentElement Position and SizeMutationObserverForm Element AccessBatch Insertion: DocumentFragment

Selecting Elements

querySelector and querySelectorAll use CSS selectors for all element selection. Always guard against null.

querySelector — first match
const btn = document.querySelector('.submit-btn');
const header = document.querySelector('#main-header');
const input = document.querySelector('input[type="email"]');
querySelectorAll — all matches
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.

Scoped selection
const nav = document.querySelector('nav');
const links = nav?.querySelectorAll('a');
// Searches only within <nav>, not the whole document
Guard against null
const 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.

createElement + textContent
const li = document.createElement('li');
li.textContent = 'New item';
li.classList.add('list-item');
li.dataset.id = '42';
Insert relative to target
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 sibling
insertAdjacentHTML — trusted HTML strings
const 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.

Remove elements
const item = document.querySelector('.remove-me');
item?.remove();  // removes from DOM

// Remove all children
const container = document.querySelector('#list');
container.replaceChildren();  // empties container

classList API

Manage CSS classes without string manipulation. Cleaner and safer than modifying className directly.

add, remove, toggle
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 boolean
contains and replace
el.classList.contains('active')     // => true/false
el.classList.replace('old', 'new')  // swap in one step
Multiple toggles for tab switching
function 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.

Read and write data attributes
// 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">
Use in event delegation
list.addEventListener('click', (e) => {
  const item = e.target.closest('[data-id]');
  if (!item) return;
  const id = item.dataset.id;
  handleClick(id);
});
Select by data attribute
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.

addEventListener vs onclick
// Recommended: addEventListener (multiple handlers OK)
btn.addEventListener('click', handleClick);
btn.addEventListener('click', logClick);

// Old style: onclick (overwrites previous handler)
btn.onclick = handleClick;  // logClick lost!
Event object properties
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
});
Remove listener
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 listener
once and passive options
btn.addEventListener('click', handler, { once: true });
// Fires once, then auto-removes

window.addEventListener('scroll', onScroll, { passive: true });
// Cannot call preventDefault() — browser can optimize scrolling

Event Delegation

Attach one listener on a parent instead of per-item listeners. Uses event bubbling and closest() for targeting.

Delegation pattern
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);
});
Why delegation over per-item listeners
// 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.

Multiple action buttons in a list
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.

textContent — safe for user input
const output = document.querySelector('#output');
const userInput = '<script>alert("xss")</script>';
output.textContent = userInput;
// Renders as visible text, NOT executed HTML
innerHTML — parses HTML (trusted only)
const 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
innerHTML destroys event listeners
// 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.

getBoundingClientRect()
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 + width
Element dimensions
element.offsetWidth   // width + padding + border
element.offsetHeight  // height + padding + border
element.clientWidth   // width + padding (no border)
element.clientHeight  // height + padding (no border)
Scroll position
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' });
Computed styles
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 stylesheet

MutationObserver

Watch for DOM changes asynchronously. Useful for responding to third-party code, lazy-loaded content, or framework-managed DOM.

Observe child list changes
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,
});
Observe attribute changes
observer.observe(element, {
  attributes: true,
  attributeFilter: ['class', 'data-state'],
});
Observe subtree and disconnect
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.

FormData — collect all fields
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);
});
Direct property access
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';
Validation
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 UI
DOMContentLoaded for safe access
document.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.

Batch insert list items
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 items
Template element alternative
const 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.

Learn JavaScript in Depth
JavaScript Functions Practice →JavaScript Collections Practice →JavaScript Error Handling Practice →
Warm-up1 / 2

Can you write this from memory?

Select the element with id "main-title".

See Also
Async/Await →String Methods →

Start Practicing JavaScript

Free daily exercises with spaced repetition. No credit card required.

← Back to JavaScript Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.