JavaScript DOM Manipulation: The Complete Guide for 2026
Every interactive web page depends on the Document Object Model. When you click a button and a modal appears, when you type in a search box and results filter in real time, when a page smoothly loads more content as you scroll — all of that is DOM manipulation. It is the bridge between your JavaScript code and what the user sees and interacts with on screen.
Despite the rise of frameworks like React and Vue, understanding the DOM API directly remains essential. Frameworks abstract the DOM, but they do not replace it. When you need to build a custom component, debug a rendering issue, optimize performance, or work outside a framework context, raw DOM knowledge is what separates a confident developer from one who is guessing.
This guide covers everything you need to know about DOM manipulation in 2026: selecting elements, creating and removing nodes, handling events, traversing the tree, observing changes, and writing performant code. Every section includes practical code examples you can use immediately.
Table of Contents
1. What is the DOM?
The Document Object Model is a programming interface for HTML documents. When a browser loads an HTML page, it parses the markup and builds a tree of objects — the DOM tree. Each HTML element becomes a node in this tree, and JavaScript can read, modify, add, or remove any node at any time.
<!-- This HTML... -->
<div id="app">
<h1>Hello</h1>
<p>Welcome to the <strong>DOM</strong>.</p>
</div>
// ...becomes this tree of objects:
// document
// └── html
// ├── head
// └── body
// └── div#app
// ├── h1
// │ └── "Hello" (text node)
// └── p
// ├── "Welcome to the " (text node)
// ├── strong
// │ └── "DOM" (text node)
// └── "." (text node)
The key insight is that the DOM is live. When you change a node with JavaScript, the browser immediately updates the page. This makes the DOM the foundation of all dynamic web behavior.
Node Types
// The most common node types:
// 1 (Element) — div, span, p, etc.
// 3 (Text) — text content inside elements
// 9 (Document) — the document itself
const div = document.createElement('div');
console.log(div.nodeType); // 1 (Node.ELEMENT_NODE)
console.log(div.nodeName); // "DIV"
// Check node type
if (node.nodeType === Node.ELEMENT_NODE) { /* element */ }
if (node.nodeType === Node.TEXT_NODE) { /* text */ }
2. Selecting Elements
Before you can manipulate an element, you need to select it. Modern JavaScript provides several methods, but querySelector and querySelectorAll are the workhorses you will use most.
querySelector and querySelectorAll
// querySelector returns the FIRST matching element (or null)
const header = document.querySelector('h1');
const nav = document.querySelector('.nav-links');
const email = document.querySelector('#email');
const submitBtn = document.querySelector('form button[type="submit"]');
// querySelectorAll returns ALL matching elements as a NodeList
const items = document.querySelectorAll('.list-item');
const links = document.querySelectorAll('nav a');
const inputs = document.querySelectorAll('input[type="text"]');
// NodeList is iterable with forEach, for...of, and spread
items.forEach(item => console.log(item.textContent));
for (const link of links) {
link.style.color = 'blue';
}
// Convert to array for full array methods
const inputArray = [...inputs];
const values = inputArray.map(input => input.value);
// Any valid CSS selector works
document.querySelector('div > p:first-child');
document.querySelector('[data-role="admin"]');
document.querySelector('.sidebar .widget:nth-child(3)');
document.querySelector('input:not([disabled])');
getElementById, getElementsByClassName, getElementsByTagName
// getElementById — fastest single-element selector
const app = document.getElementById('app');
// Note: no # prefix, just the id string
// getElementsByClassName — returns a live HTMLCollection
const cards = document.getElementsByClassName('card');
// Live: if you add a .card element, it automatically appears in the collection
// getElementsByTagName — all elements of a tag
const paragraphs = document.getElementsByTagName('p');
const allElements = document.getElementsByTagName('*');
// HTMLCollection vs NodeList:
// - HTMLCollection (getElementsBy*) is LIVE — updates automatically
// - NodeList (querySelectorAll) is STATIC — a snapshot at query time
// - HTMLCollection has NO forEach — convert to array first
const liveCards = document.getElementsByClassName('card');
console.log(liveCards.length); // 3
document.body.appendChild(document.createElement('div')).className = 'card';
console.log(liveCards.length); // 4 — it updated automatically!
// Convert HTMLCollection to array
const cardArray = [...cards];
const cardArray2 = Array.from(cards);
Closest, Matches, and Contains
// closest() — find the nearest ancestor matching a selector
const button = document.querySelector('.delete-btn');
const card = button.closest('.card'); // nearest .card ancestor
const missing = button.closest('.nonexistent'); // null
// matches() — check if an element matches a selector
el.matches('.item.active'); // true if it has both classes
// contains() — check if one element is inside another
parent.contains(child); // true if child is inside parent
3. Creating and Inserting Elements
Creating new elements and inserting them into the DOM is fundamental to building dynamic interfaces. There are multiple approaches, each with its own strengths.
createElement and appendChild
// Create a new element
const card = document.createElement('div');
card.className = 'card';
card.id = 'card-1';
const title = document.createElement('h2');
title.textContent = 'New Card';
const body = document.createElement('p');
body.textContent = 'This card was created with JavaScript.';
// Build the tree and insert into the page
card.appendChild(title);
card.appendChild(body);
document.querySelector('.container').appendChild(card);
// append() accepts multiple nodes at once
function createUserCard(user) {
const card = document.createElement('div');
card.className = 'user-card';
const name = document.createElement('h3');
name.textContent = user.name;
const email = document.createElement('p');
email.textContent = user.email;
card.append(name, email);
return card;
}
insertAdjacentHTML
// Insert HTML string at a specific position
// Positions: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
const list = document.querySelector('.list');
list.insertAdjacentHTML('afterbegin', '<li>First</li>'); // first child
list.insertAdjacentHTML('beforeend', '<li>Last</li>'); // last child
list.insertAdjacentHTML('beforebegin', '<h3>Title</h3>'); // before element
list.insertAdjacentHTML('afterend', '<p>Footer</p>'); // after element
// Visual guide:
// <!-- beforebegin -->
// <ul class="list">
// <!-- afterbegin -->
// <li>existing item</li>
// <!-- beforeend -->
// </ul>
// <!-- afterend -->
prepend, append, before, after, replaceWith
// Modern insertion methods (accept multiple nodes AND strings)
const container = document.querySelector('.container');
container.prepend('Text ', document.createElement('hr')); // beginning (inside)
container.append(document.createElement('div'), ' text'); // end (inside)
target.before(document.createElement('p')); // before the element (outside)
target.after('Some text after'); // after the element (outside)
oldCard.replaceWith(newCard); // replace the element entirely
// Clone an element
const clone = template.cloneNode(true); // true = deep clone (with children)
clone.querySelector('h2').textContent = 'Cloned Card';
container.appendChild(clone);
4. Modifying Elements
Once you have a reference to an element, you can change its content, attributes, styles, and classes.
Text Content and HTML
const el = document.querySelector('.message');
// textContent — get/set plain text (safe, no HTML parsing)
el.textContent = 'Hello, world!';
// innerHTML — get/set HTML content (parses HTML tags)
el.innerHTML = '<strong>Bold</strong> and <em>italic</em>';
// WARNING: never use innerHTML with user input (XSS risk!)
// innerText — like textContent but respects CSS visibility (triggers reflow)
// outerHTML — includes the element itself in the HTML string
Attributes
const link = document.querySelector('a');
// Standard attributes — use properties directly
link.href = 'https://example.com';
link.target = '_blank';
// getAttribute/setAttribute — for any attribute
link.setAttribute('aria-label', 'Visit Example');
link.hasAttribute('target'); // true
link.removeAttribute('target');
// data-* attributes — use the dataset property
const card = document.querySelector('.card');
card.dataset.userId = '42'; // sets data-user-id="42"
card.dataset.role = 'admin'; // sets data-role="admin"
console.log(card.dataset.userId); // "42"
delete card.dataset.role; // removes data-role
// Note: data-user-id in HTML becomes dataset.userId in JS (camelCase)
classList
const el = document.querySelector('.box');
el.classList.add('active'); // add one class
el.classList.add('highlight', 'visible'); // add multiple
el.classList.remove('hidden'); // remove
el.classList.toggle('active'); // add if missing, remove if present
el.classList.toggle('dark', isDarkMode); // conditional: true=add, false=remove
el.classList.contains('active'); // check if exists
el.classList.replace('old', 'new'); // replace one class with another
Inline Styles
const el = document.querySelector('.box');
// Set individual styles (camelCase property names)
el.style.backgroundColor = '#3b82f6';
el.style.padding = '1rem';
el.style.borderRadius = '8px';
// Set multiple styles at once
el.style.cssText = 'background: red; padding: 1rem; display: flex;';
// Read computed styles (including inherited and stylesheet values)
const computed = getComputedStyle(el);
console.log(computed.fontSize); // "16px"
// CSS custom properties
el.style.setProperty('--card-color', '#3b82f6');
getComputedStyle(el).getPropertyValue('--card-color');
5. Removing Elements
// remove() — the modern way
document.querySelector('.card').remove();
// removeChild() — traditional (needs parent reference)
list.removeChild(list.querySelector('li'));
// Remove all children
container.replaceChildren(); // modern, clean
container.innerHTML = ''; // fast but loses event listeners
// Remove elements matching a condition
document.querySelectorAll('.item.completed').forEach(el => el.remove());
// Remove after animation
toast.classList.add('fade-out');
toast.addEventListener('animationend', () => toast.remove());
6. DOM Traversal
DOM traversal means navigating between nodes — moving from parent to child, between siblings, or upward to ancestors. There are two sets of properties: one that includes all nodes (including text and comments), and one that includes only element nodes.
// Given this HTML:
// <ul id="menu">
// <li>Home</li>
// <li class="active">About</li>
// <li>Contact</li>
// </ul>
const menu = document.querySelector('#menu');
const active = document.querySelector('.active');
// ELEMENT-ONLY traversal (what you want 99% of the time)
menu.children; // HTMLCollection of li elements
menu.firstElementChild; // first li (Home)
menu.lastElementChild; // last li (Contact)
menu.childElementCount; // 3
active.parentElement; // the ul#menu
active.nextElementSibling; // li (Contact)
active.previousElementSibling; // li (Home)
// ALL-NODE traversal (includes text nodes, whitespace, comments)
// childNodes, firstChild, lastChild, nextSibling, previousSibling
// Use these only when you need text nodes; prefer element traversal above
// Useful helpers
const getSiblings = el =>
[...el.parentElement.children].filter(child => child !== el);
function walkTree(root, callback) {
callback(root);
for (const child of root.children) walkTree(child, callback);
}
7. Event Handling
Events are the mechanism that makes web pages interactive. Every user action — clicking, typing, scrolling, hovering — fires an event that you can listen for and respond to.
addEventListener
const button = document.querySelector('#submit');
// Basic event listener
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Target:', event.target);
});
// Arrow function (common)
button.addEventListener('click', (e) => {
e.preventDefault(); // prevent default browser behavior
console.log('Clicked');
});
// Named function (removable)
function handleClick(e) {
console.log('Clicked at', e.clientX, e.clientY);
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // exact same reference required
// Options object
button.addEventListener('click', handleClick, {
once: true, // automatically removes after first call
passive: true, // promises not to call preventDefault (performance)
capture: true // fires during capture phase instead of bubble phase
});
// Common event types: click, dblclick, mouseenter, mouseleave,
// keydown, keyup, input, change, submit, focus, blur, scroll
The Event Object
document.addEventListener('click', (e) => {
e.target; // the element that was clicked
e.currentTarget; // the element the listener is attached to
e.clientX, e.clientY; // mouse position relative to viewport
e.shiftKey, e.ctrlKey, e.altKey, e.metaKey; // modifier keys
e.preventDefault(); // stop default behavior
e.stopPropagation(); // stop bubbling to parent elements
});
document.addEventListener('keydown', (e) => {
e.key; // "Enter", "Escape", "a", "ArrowUp"
e.code; // "KeyA" (physical key, layout-independent)
if (e.key === 'Escape') closeModal();
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveDocument();
}
});
Event Delegation
// BAD: individual listeners on 100 items
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleItemClick);
});
// Problems: memory-heavy, doesn't work for dynamically added items
// GOOD: one listener on the parent
const list = document.querySelector('.item-list');
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (!item) return; // click was not on an item
if (!list.contains(item)) return; // safety check
console.log('Clicked item:', item.dataset.id);
item.classList.toggle('selected');
});
// Works for lists, tables, grids, and any dynamically generated content
8. Form Handling with the DOM
// Access form elements
const form = document.querySelector('#signup-form');
// Listen for form submission
form.addEventListener('submit', (e) => {
e.preventDefault(); // prevent page reload
// Access individual fields
const name = form.querySelector('#name').value;
const email = form.querySelector('#email').value;
// Or use FormData for all fields at once
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// { name: "Alice", email: "alice@example.com", role: "developer" }
// Validate
if (!name.trim()) {
showError('Name is required');
return;
}
// Submit
fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
});
// Real-time validation
emailInput.addEventListener('input', (e) => {
const isValid = e.target.value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
e.target.classList.toggle('invalid', !isValid);
});
// Reset form and access different input types
form.reset();
document.querySelector('#country').value; // select value
document.querySelector('#agree').checked; // checkbox
[...document.querySelectorAll('[name="plan"]')]
.find(r => r.checked)?.value; // radio value
9. DOM Mutation Observer
MutationObserver watches for changes to the DOM and calls your callback when nodes are added, removed, or modified. It replaced the deprecated Mutation Events API and is far more performant.
// Watch for child elements being added or removed
const container = document.querySelector('#content');
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('Added:', node.tagName, node.className);
}
});
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('Removed:', node.tagName);
}
});
}
}
});
observer.observe(container, {
childList: true, // watch for added/removed children
subtree: true // watch all descendants, not just direct children
});
// Watch for attribute changes
const badge = document.querySelector('.badge');
const attrObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes') {
console.log(`${mutation.attributeName} changed to`,
badge.getAttribute(mutation.attributeName));
}
}
});
attrObserver.observe(badge, {
attributes: true,
attributeFilter: ['class', 'data-count'] // only these attributes
});
// Stop observing when done
observer.disconnect();
10. Performance Best Practices
DOM manipulation is one of the most expensive operations in the browser. Every change can trigger layout recalculation (reflow), style computation, and repainting. Writing performant DOM code means minimizing these operations.
DocumentFragment
// BAD: 1000 individual DOM insertions (1000 reflows)
const list = document.querySelector('.list');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
list.appendChild(li); // triggers reflow each time
}
// GOOD: build in a DocumentFragment, insert once (1 reflow)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li); // no reflow — fragment is off-screen
}
list.appendChild(fragment); // single reflow for all 1000 items
Batch DOM Reads and Writes
// BAD: interleaving reads and writes forces multiple reflows
const boxes = document.querySelectorAll('.box');
boxes.forEach(box => {
const height = box.offsetHeight; // READ — forces reflow
box.style.height = height * 2 + 'px'; // WRITE — invalidates layout
// Next iteration: READ forces another reflow because layout is dirty
});
// GOOD: batch all reads first, then all writes
const heights = [];
boxes.forEach(box => {
heights.push(box.offsetHeight); // all READs together
});
boxes.forEach((box, i) => {
box.style.height = heights[i] * 2 + 'px'; // all WRITEs together
});
// Only one reflow for the entire batch
requestAnimationFrame
// Use requestAnimationFrame for visual updates
function animateProgress(target) {
let current = 0;
function step() {
current += 2;
progressBar.style.width = current + '%';
if (current < target) {
requestAnimationFrame(step); // syncs with display refresh
}
}
requestAnimationFrame(step);
}
// Debounce scroll/resize handlers with rAF
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updateHeaderOnScroll();
ticking = false;
});
ticking = true;
}
});
Cache DOM References
// BAD: querying the DOM inside a loop
function updateItems(data) {
data.forEach(item => {
// querySelector runs every iteration
document.querySelector('.container').appendChild(/* ... */);
});
}
// GOOD: cache the reference
function updateItems(data) {
const container = document.querySelector('.container');
const fragment = document.createDocumentFragment();
data.forEach(item => {
const el = document.createElement('div');
el.textContent = item.name;
fragment.appendChild(el);
});
container.appendChild(fragment);
}
11. Modern DOM APIs
Intersection Observer
Intersection Observer detects when elements enter or leave the viewport. It replaces scroll event listeners for lazy loading, infinite scroll, and scroll-triggered animations.
// Lazy load images when they enter the viewport
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.removeAttribute('data-src');
imageObserver.unobserve(img); // stop watching this image
}
});
}, {
rootMargin: '200px' // start loading 200px before visible
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// Scroll-triggered animations
const animObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
entry.target.classList.toggle('visible', entry.isIntersecting);
});
}, {
threshold: 0.1 // trigger when 10% visible
});
document.querySelectorAll('.animate-on-scroll').forEach(el => {
animObserver.observe(el);
});
// Infinite scroll
const sentinel = document.querySelector('#scroll-sentinel');
const scrollObserver = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const newItems = await fetchMoreItems();
appendItems(newItems);
}
});
scrollObserver.observe(sentinel);
Resize Observer
// Watch an element's size changes (not window resize — element resize)
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Element resized: ${width}x${height}`);
// Adjust layout based on element size (container queries in JS)
if (width < 400) {
entry.target.classList.add('compact');
} else {
entry.target.classList.remove('compact');
}
}
});
const card = document.querySelector('.responsive-card');
resizeObserver.observe(card);
// Stop observing
resizeObserver.unobserve(card);
resizeObserver.disconnect(); // stop all observations
AbortController for Event Cleanup
// Remove multiple event listeners at once with AbortController
const controller = new AbortController();
const { signal } = controller;
document.addEventListener('click', handleClick, { signal });
document.addEventListener('keydown', handleKeydown, { signal });
window.addEventListener('resize', handleResize, { signal });
// Later: remove ALL listeners with one call
controller.abort();
// Great for component lifecycle
function initWidget(container) {
const controller = new AbortController();
const { signal } = controller;
container.addEventListener('click', onClick, { signal });
document.addEventListener('keydown', onKey, { signal });
return () => controller.abort(); // cleanup function
}
12. Common Patterns and Recipes
Modal Dialog
function createModal(content) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = content;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const close = () => overlay.remove();
modal.querySelector('.modal-close')?.addEventListener('click', close);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') close();
}, { once: true });
return { close };
}
Debounced Search Input
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const searchInput = document.querySelector('#search');
const results = document.querySelector('#results');
const search = debounce(async (query) => {
if (!query.trim()) { results.innerHTML = ''; return; }
const data = await fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(r => r.json());
results.innerHTML = data.map(item =>
`<div class="result"><h3>${item.title}</h3></div>`
).join('');
}, 300);
searchInput.addEventListener('input', (e) => search(e.target.value));
Dark Mode Toggle
const isDark = localStorage.getItem('theme') === 'dark' ||
(!localStorage.getItem('theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
document.querySelector('#dark-mode-toggle').addEventListener('click', () => {
const dark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', dark ? 'dark' : 'light');
});
Copy to Clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showToast('Copied!');
} catch {
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
}
Frequently Asked Questions
What is the difference between querySelector and getElementById?
getElementById selects a single element by its id attribute and is slightly faster. querySelector accepts any CSS selector (classes, attributes, pseudo-selectors, combinators) and is far more flexible. In practice, querySelector is preferred for most use cases because it handles any selector pattern, while getElementById is only useful when you have an id. Performance differences are negligible for typical applications.
What is event delegation and why should I use it?
Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on each child. When an event fires on a child, it bubbles up to the parent where your handler checks event.target to determine which child was clicked. This is more memory-efficient, works automatically with dynamically added elements, and simplifies code when you have many similar elements like list items, table rows, or buttons.
Should I use innerHTML or createElement to add elements to the DOM?
Use createElement when you need fine-grained control, when working with user input (innerHTML can introduce XSS vulnerabilities), or when you need to attach event listeners to the new elements. Use innerHTML for simple static HTML insertions where security is not a concern. For the best of both worlds, use template literals with insertAdjacentHTML, which lets you insert HTML strings at specific positions without overwriting existing content.
How do I improve DOM manipulation performance?
Batch your DOM updates to minimize reflows and repaints. Use DocumentFragment to build complex structures off-screen before inserting them. Cache DOM references instead of querying repeatedly. Use requestAnimationFrame for visual updates. Prefer classList over className for toggling classes. Use event delegation instead of individual listeners. For large lists, consider virtual scrolling with Intersection Observer to render only visible items.
What are Mutation Observer and Intersection Observer used for?
MutationObserver watches for changes to the DOM tree, such as added or removed nodes, attribute changes, and text content modifications. It is useful for building plugins, auto-saving content, and reacting to third-party DOM changes. IntersectionObserver detects when an element enters or exits the viewport, making it ideal for lazy loading images, infinite scroll, scroll-triggered animations, and tracking which content the user has actually seen.
Conclusion
The DOM API is the foundation of everything interactive in a web browser. Frameworks abstract it, but they all rely on the same underlying methods. When you understand the DOM deeply, you write better framework code, debug faster, and build things no framework covers out of the box.
Start with querySelector, textContent, classList, and addEventListener with event delegation. Then move on to DocumentFragment, batched updates, and modern observers. The best way to learn is to build things without a framework — each project reinforces the patterns in this guide.
Learn More
- JavaScript ES6+ Features: The Complete Guide — master destructuring, arrow functions, async/await, and every modern JavaScript feature
- JavaScript Array Methods: The Complete Guide — deep dive into map, filter, reduce, and every array method
- JavaScript Runner — run and test JavaScript code snippets instantly in your browser
- HTML Preview — preview HTML, CSS, and JavaScript output in real time
Related: Check out our JavaScript ES6+ Features Guide for the modern syntax used throughout this article, and our JavaScript Array Methods Guide for working with data after you extract it from the DOM.