Selecting elements
document.querySelector('.card'); // first match
document.querySelectorAll('.card'); // NodeList of all matches
document.getElementById('main'); // single element by id
document.getElementsByClassName('card');// live HTMLCollection
document.getElementsByTagName('li'); // live HTMLCollectionquerySelector/querySelectorAll accept any CSS selector, which makes them the default choice — you can target things like '.card:nth-child(2)' or 'input[type="email"]' directly.
Live vs. static collections: getElementsByClassName/getElementsByTagName return live collections — they update automatically as the DOM changes. querySelectorAll returns a static NodeList — a snapshot at the time you called it. This distinction causes subtle bugs if you’re not aware of it (e.g. looping over a live collection while removing elements from it).
querySelectorAll returns a NodeList, not an array — it has .forEach but not .map/.filter. Convert with Array.from(nodeList) or [...nodeList] if you need array methods.
Traversing the DOM
el.parentElement;
el.children; // element children only
el.childNodes; // includes text/comment nodes
el.nextElementSibling;
el.previousElementSibling;
el.closest('.container'); // nearest ancestor (or self) matching selector
el.matches('.active'); // does this element match a selector?closest() and matches() are especially useful for event delegation (below).
Reading & modifying content
el.textContent = 'Hello'; // plain text, safe from HTML injection
el.innerHTML = '<b>Hi</b>'; // parses as HTML — sanitize any untrusted input first
el.innerText; // like textContent but respects rendered styling (slower, layout-aware)Prefer textContent over innerHTML unless you specifically need to insert markup — innerHTML is a common XSS vector if the content comes from user input.
Attributes & properties
el.getAttribute('data-id');
el.setAttribute('data-id', '42');
el.removeAttribute('disabled');
el.hasAttribute('hidden');
el.dataset.id; // reads data-id
el.dataset.userName; // reads data-user-name (camelCase <-> kebab-case)dataset is the ergonomic way to work with data-* attributes — no manual string parsing.
Note: some HTML attributes and DOM properties diverge. el.value (a form input’s current value) and el.getAttribute('value') (the attribute set in HTML) can differ once the user types.
Classes & styles
el.classList.add('active');
el.classList.remove('active');
el.classList.toggle('active');
el.classList.toggle('active', someBoolean); // force add/remove based on condition
el.classList.contains('active');
el.style.color = 'red'; // inline style — use sparingly
el.style.setProperty('--gap', '8px'); // set a CSS custom propertyPrefer toggling classes over setting el.style directly — keeps styling in CSS where it belongs, and plays nicer with your design tokens.
Creating & inserting elements
const div = document.createElement('div');
div.textContent = 'New card';
div.className = 'card';
parent.appendChild(div);
parent.prepend(div);
parent.append(div, anotherEl); // append() accepts multiple nodes/strings
el.before(newEl);
el.after(newEl);
el.replaceWith(newEl);
el.remove();For inserting HTML strings directly:
el.insertAdjacentHTML('beforeend', '<li>Item</li>');insertAdjacentHTML positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend' — before/after the element itself, or just inside its first/last child position.
Performance note: if you’re inserting many elements in a loop, build them into a DocumentFragment first and append the fragment once — this avoids triggering a reflow/repaint on every single insertion.
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
list.appendChild(fragment);Events
el.addEventListener('click', (event) => {
console.log(event.target); // the actual element clicked
});
el.removeEventListener('click', handlerFn); // must be the same function referenceEvent delegation — attach one listener to a parent instead of many to children, and check what was actually clicked:
list.addEventListener('click', (event) => {
const item = event.target.closest('.list-item');
if (!item) return;
// handle click on item
});This is more efficient for long/dynamic lists and automatically covers elements added later.
Useful event methods:
event.preventDefault(); // stop default browser behavior (e.g. form submit, link navigation)
event.stopPropagation(); // stop the event from bubbling further upEvent flow: events capture down from window to the target, then bubble back up. addEventListener listens on the bubble phase by default; pass { capture: true } as a third argument to listen during capture instead.
Observing changes
For reacting to DOM changes that don’t come from your own code (e.g. content injected by a third-party script, or watching for an element to appear):
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => console.log(m));
});
observer.observe(targetEl, { childList: true, subtree: true, attributes: true });
observer.disconnect(); // stop observingFor reacting to an element entering/leaving the viewport (common for lazy-loading, infinite scroll, animations on scroll):
const io = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) { /* element is visible */ }
});
});
io.observe(targetEl);Quick reference: common gotchas
querySelectorAllresult is static;getElementsByClassNameresult is live.innerHTMLparses HTML — never insert untrusted content that way.el.style.xonly reads/sets inline styles, not computed/CSS-file styles. UsegetComputedStyle(el)to read the actual rendered value.- Removing an event listener requires the exact same function reference used to add it — anonymous inline functions can’t be removed.
- Inserting many nodes one at a time in a loop is slow; batch with a
DocumentFragment.