JavaScript — DOM Manipulation
The Document Object Model (DOM) is a programming interface for web documents. It represents the page as a tree of nodes, where each node corresponds to a part of the document — elements, attributes, text content, and more. JavaScript uses the DOM to interact with and modify web pages dynamically.
When a browser loads an HTML page, it parses the HTML and constructs the DOM tree. Every HTML element becomes an element node in this tree. The DOM provides methods and properties to query, traverse, create, modify, and delete these nodes programmatically.
The DOM is not part of JavaScript itself — it is a Web API provided by the browser environment. This is why JavaScript running in Node.js does not have access to the document object or DOM methods. Understanding the DOM is essential for any client-side web developer.
| 1 | // The starting point — the document object |
| 2 | console.log(document); // The entire HTML document |
| 3 | console.log(document.title); // The page title |
| 4 | console.log(document.URL); // The current URL |
| 5 | console.log(document.head); // The <head> element |
| 6 | console.log(document.body); // The <body> element |
| 7 | |
| 8 | // The DOM is a tree rooted at document |
| 9 | // document → html → head + body |
| 10 | // body → div → p → text |
| 11 | |
| 12 | // Every element is a node with properties and methods |
| 13 | const body = document.body; |
| 14 | console.log(body.nodeType); // 1 (ELEMENT_NODE) |
| 15 | console.log(body.nodeName); // "BODY" |
| 16 | console.log(body.tagName); // "BODY" |
The DOM tree consists of several types of nodes. The most common are element nodes, text nodes, and the document node itself. Understanding the different node types helps you navigate and manipulate the tree correctly.
| Node Type | nodeType | Description | Example |
|---|---|---|---|
| Document | 9 | The root document object | document |
| Element | 1 | An HTML element (div, p, a, etc.) | <div> |
| Attribute | 2 | An attribute of an element (deprecated) | class="foo" |
| Text | 3 | Actual text content in an element | "Hello" |
| Comment | 8 | HTML comment node | <!-- comment --> |
| DocumentFragment | 11 | Lightweight document for batch operations | document.createDocumentFragment() |
| 1 | // Visualizing the DOM tree structure |
| 2 | // HTML: <div id="app"><h1>Title</h1><p>Text</p></div> |
| 3 | |
| 4 | const app = document.getElementById("app"); |
| 5 | |
| 6 | // The DOM tree for the above HTML: |
| 7 | // Document |
| 8 | // └─ html |
| 9 | // └─ body |
| 10 | // └─ div#app |
| 11 | // ├─ h1 |
| 12 | // │ └─ "Title" (text node) |
| 13 | // └─ p |
| 14 | // └─ "Text" (text node) |
| 15 | |
| 16 | // Navigating the tree |
| 17 | console.log(app.childNodes); // NodeList: [h1, text(" |
| 18 | "), p] |
| 19 | console.log(app.children); // HTMLCollection: [h1, p] |
| 20 | console.log(app.firstChild); // h1 (first child node) |
| 21 | console.log(app.firstElementChild); // h1 (first element child) |
| 22 | console.log(app.lastChild); // text node (newline) |
| 23 | console.log(app.lastElementChild); // p |
| 24 | console.log(app.parentNode); // body |
| 25 | console.log(app.parentElement); // body |
info
Before you can manipulate an element, you need to select it. The DOM provides several methods for finding elements, from simple ID lookups to powerful CSS selector-based queries.
| Method | Returns | Performance | Flexibility | Best For |
|---|---|---|---|---|
| getElementById | Single element | Fastest | Low (ID only) | Unique elements with IDs |
| querySelector | Single element | Fast | High (any CSS selector) | First matching element |
| querySelectorAll | NodeList (static) | Fast | High (any CSS selector) | Multiple matching elements |
| getElementsByClassName | HTMLCollection (live) | Fast | Medium (class name only) | Live collections of elements |
| getElementsByTagName | HTMLCollection (live) | Fast | Low (tag name only) | All elements of a type |
| 1 | // getElementById — single element by ID (fastest) |
| 2 | const header = document.getElementById("header"); |
| 3 | console.log(header); // <header id="header">...</header> |
| 4 | |
| 5 | // querySelector — first match using CSS selector |
| 6 | const firstButton = document.querySelector(".btn-primary"); |
| 7 | const navItem = document.querySelector("nav ul li:first-child"); |
| 8 | const form = document.querySelector("#signup-form"); |
| 9 | |
| 10 | // querySelectorAll — all matches (static NodeList) |
| 11 | const allButtons = document.querySelectorAll("button"); |
| 12 | const cards = document.querySelectorAll(".card.featured"); |
| 13 | console.log(cards.length); // number of matching elements |
| 14 | |
| 15 | // querySelectorAll returns a NodeList (NOT an array) |
| 16 | // It has forEach but not map/filter/reduce |
| 17 | cards.forEach((card) => console.log(card)); |
| 18 | |
| 19 | // Convert NodeList to Array for full Array methods |
| 20 | const cardArray = Array.from(cards); |
| 21 | const cardArray2 = [...cards]; // spread works on NodeLists |
| 22 | |
| 23 | // getElementsByClassName — live HTMLCollection |
| 24 | const items = document.getElementsByClassName("item"); |
| 25 | console.log(items.length); // updates automatically when DOM changes |
| 26 | |
| 27 | // getElementsByTagName — live HTMLCollection |
| 28 | const paragraphs = document.getElementsByTagName("p"); |
| 29 | |
| 30 | // Important: live collections reflect DOM changes in real time |
| 31 | // If you remove a matching element from the DOM, it's gone from the collection |
| 32 | // If you add a new matching element, it appears in the collection |
best practice
Once you have a reference to an element, you can navigate to related elements using traversal properties. These allow you to move up, down, and sideways through the DOM tree.
| Property/Method | Direction | Returns | Description |
|---|---|---|---|
| parentNode | Up | Node or null | Parent node (any type) |
| parentElement | Up | Element or null | Parent element node only |
| children | Down | HTMLCollection | Child elements only (no text nodes) |
| childNodes | Down | NodeList | All child nodes (including text) |
| firstElementChild | Down | Element or null | First child element |
| lastElementChild | Down | Element or null | Last child element |
| nextElementSibling | Sideways | Element or null | Next sibling element |
| previousElementSibling | Sideways | Element or null | Previous sibling element |
| closest(selector) | Up | Element or null | Nearest ancestor matching CSS selector |
| matches(selector) | Check | boolean | Checks if element matches CSS selector |
| 1 | // Starting point |
| 2 | const activeLink = document.querySelector("nav a.active"); |
| 3 | |
| 4 | // Traversing UP |
| 5 | console.log(activeLink.parentNode); // <li> or parent node |
| 6 | console.log(activeLink.parentElement); // <li> (element only) |
| 7 | console.log(activeLink.closest("nav")); // <nav> — nearest matching ancestor |
| 8 | console.log(activeLink.closest("li")); // <li> |
| 9 | console.log(activeLink.closest("body")); // <body> |
| 10 | |
| 11 | // Traversing DOWN |
| 12 | const nav = document.querySelector("nav"); |
| 13 | console.log(nav.children); // HTMLCollection of child elements |
| 14 | console.log(nav.firstElementChild); // First child element |
| 15 | console.log(nav.lastElementChild); // Last child element |
| 16 | console.log(nav.childNodes); // All nodes including text/whitespace |
| 17 | |
| 18 | // Traversing SIDEWAYS |
| 19 | console.log(activeLink.nextElementSibling); // Next <a> in same parent |
| 20 | console.log(activeLink.previousElementSibling); // Previous <a> |
| 21 | |
| 22 | // Checking with matches() |
| 23 | console.log(activeLink.matches("a")); // true |
| 24 | console.log(activeLink.matches(".active")); // true |
| 25 | console.log(activeLink.matches("nav a:first-child")); // check by position |
| 26 | |
| 27 | // Practical: find the section containing a specific element |
| 28 | function findSection(element) { |
| 29 | return element.closest("section"); |
| 30 | } |
| 31 | const section = findSection(activeLink); |
| 32 | console.log(section?.id); // the section id |
| 33 | |
| 34 | // Practical: check if element is visible |
| 35 | function isVisible(el) { |
| 36 | return el && el.matches(":visible") === false; |
| 37 | } |
pro tip
Once you have selected an element, you can modify its content, attributes, and appearance. The DOM provides a complete set of properties and methods for element manipulation.
Content Manipulation
| 1 | // textContent — plain text (safe, no HTML parsing) |
| 2 | const div = document.querySelector(".content"); |
| 3 | div.textContent = "Hello, World!"; // Sets text |
| 4 | console.log(div.textContent); // Returns text content |
| 5 | |
| 6 | // innerHTML — HTML content (parses HTML — be careful with user input) |
| 7 | div.innerHTML = "<strong>Bold text</strong>"; // Parses HTML |
| 8 | div.innerHTML += "<p>Appended paragraph</p>"; // Appends HTML (bad practice) |
| 9 | |
| 10 | // innerText — respects CSS visibility (slower than textContent) |
| 11 | div.innerText = "Visible text"; // Respects display:none |
| 12 | |
| 13 | // Attribute manipulation |
| 14 | const link = document.querySelector("a"); |
| 15 | link.href = "https://example.com"; |
| 16 | link.setAttribute("target", "_blank"); |
| 17 | link.getAttribute("href"); // "https://example.com" |
| 18 | link.hasAttribute("target"); // true |
| 19 | link.removeAttribute("target"); // removes the attribute |
| 20 | |
| 21 | // data-* attributes |
| 22 | const card = document.querySelector(".card"); |
| 23 | card.dataset.id = "123"; // sets data-id="123" |
| 24 | card.dataset.userRole = "admin"; // sets data-user-role="admin" |
| 25 | console.log(card.dataset.id); // "123" |
| 26 | console.log(card.getAttribute("data-user-role")); // "admin" |
| 27 | |
| 28 | // Style property (inline styles) |
| 29 | const box = document.querySelector(".box"); |
| 30 | box.style.color = "#00FF41"; |
| 31 | box.style.backgroundColor = "#0A0A0A"; |
| 32 | box.style.fontSize = "14px"; |
| 33 | // Note: CSS property names become camelCase in JavaScript |
Class Manipulation
| 1 | // className — replaces all classes (avoid, use classList) |
| 2 | const el = document.querySelector(".card"); |
| 3 | console.log(el.className); // "card featured" |
| 4 | |
| 5 | // classList — modern API (recommended) |
| 6 | el.classList.add("highlighted"); // Add a class |
| 7 | el.classList.remove("featured"); // Remove a class |
| 8 | el.classList.toggle("expanded"); // Toggle on/off |
| 9 | el.classList.replace("old-class", "new-class"); // Replace |
| 10 | console.log(el.classList.contains("card")); // true — check existence |
| 11 | |
| 12 | // Multiple classes at once |
| 13 | el.classList.add("is-active", "is-visible"); |
| 14 | el.classList.remove("hidden", "collapsed"); |
| 15 | |
| 16 | // Practical: toggle visibility |
| 17 | function toggleVisibility(element) { |
| 18 | element.classList.toggle("hidden"); |
| 19 | } |
| 20 | |
| 21 | // Practical: conditional class |
| 22 | function setActive(element, isActive) { |
| 23 | element.classList.toggle("active", isActive); |
| 24 | } |
warning
Creating new elements and removing existing ones is a fundamental DOM operation. Modern DOM APIs have simplified these tasks significantly compared to older approaches.
Creating Elements
| 1 | // Create a new element |
| 2 | const newDiv = document.createElement("div"); |
| 3 | newDiv.textContent = "I was created dynamically!"; |
| 4 | newDiv.className = "dynamic-card"; |
| 5 | |
| 6 | // Set attributes during creation |
| 7 | const link = document.createElement("a"); |
| 8 | link.href = "https://example.com"; |
| 9 | link.textContent = "Visit Example"; |
| 10 | link.target = "_blank"; |
| 11 | |
| 12 | // Create text node (usually implicit via textContent) |
| 13 | const textNode = document.createTextNode("Some text"); |
| 14 | |
| 15 | // Clone an existing element |
| 16 | const original = document.querySelector(".card"); |
| 17 | const clone = original.cloneNode(true); // true = deep clone (includes children) |
Inserting Elements
| 1 | // appendChild — adds as last child |
| 2 | const list = document.querySelector("ul"); |
| 3 | const newItem = document.createElement("li"); |
| 4 | newItem.textContent = "New item"; |
| 5 | list.appendChild(newItem); |
| 6 | |
| 7 | // insertBefore — inserts before a reference node |
| 8 | const firstItem = list.firstElementChild; |
| 9 | const anotherItem = document.createElement("li"); |
| 10 | anotherItem.textContent = "Inserted first"; |
| 11 | list.insertBefore(anotherItem, firstItem); |
| 12 | |
| 13 | // Modern append/prepend (works with multiple arguments and strings) |
| 14 | const list2 = document.querySelector(".list"); |
| 15 | list2.append("Text node", document.createElement("span")); // after last child |
| 16 | list2.prepend(document.createElement("i")); // before first child |
| 17 | |
| 18 | // Modern before/after/insertAdjacentElement |
| 19 | const ref = document.querySelector(".ref-element"); |
| 20 | ref.before(document.createElement("hr")); // inserts before the element |
| 21 | ref.after(document.createElement("hr")); // inserts after the element |
| 22 | ref.replaceWith(document.createElement("div")); // replaces the element |
| 23 | |
| 24 | // beforebegin, afterbegin, beforeend, afterend |
| 25 | ref.insertAdjacentHTML("afterend", "<p>After the element</p>"); |
Removing Elements
| 1 | // remove() — modern, direct (recommended) |
| 2 | const element = document.querySelector(".to-remove"); |
| 3 | element.remove(); |
| 4 | |
| 5 | // removeChild — older approach |
| 6 | const parent = document.querySelector(".container"); |
| 7 | const child = parent.querySelector(".to-remove"); |
| 8 | parent.removeChild(child); |
| 9 | |
| 10 | // Replace a child |
| 11 | const oldChild = parent.querySelector(".old"); |
| 12 | const newChild = document.createElement("div"); |
| 13 | newChild.textContent = "Replacement"; |
| 14 | parent.replaceChild(newChild, oldChild); |
| 15 | |
| 16 | // Clear all children |
| 17 | function clearElement(el) { |
| 18 | while (el.firstChild) { |
| 19 | el.removeChild(el.firstChild); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | // Alternative: replace innerHTML (simpler but less efficient) |
| 24 | element.innerHTML = ""; |
| 25 | |
| 26 | // Alternative: replaceChildren (modern) |
| 27 | element.replaceChildren(); // removes all children |
| 28 | element.replaceChildren(newDiv, newSpan); // replaces with new children |
info
JavaScript can manipulate CSS styles in several ways: through the style property, by managing CSS classes, or by reading computed styles. Each approach has appropriate use cases.
Inline Styles via style Property
| 1 | // Setting inline styles (camelCase property names) |
| 2 | const box = document.querySelector(".box"); |
| 3 | box.style.color = "#00FF41"; |
| 4 | box.style.backgroundColor = "#0A0A0A"; |
| 5 | box.style.borderRadius = "8px"; |
| 6 | box.style.padding = "16px"; |
| 7 | |
| 8 | // CSS custom properties (CSS variables) |
| 9 | box.style.setProperty("--brand", "#00FF41"); |
| 10 | box.style.setProperty("--spacing", "1.5rem"); |
| 11 | |
| 12 | // Reading inline styles |
| 13 | console.log(box.style.color); // "#00FF41" |
| 14 | console.log(box.style.getPropertyValue("--brand")); // "#00FF41" |
| 15 | |
| 16 | // Remove a style |
| 17 | box.style.removeProperty("color"); // removes inline color |
| 18 | // Setting to empty string also works |
| 19 | box.style.backgroundColor = ""; |
| 20 | |
| 21 | // Multiple styles at once (use cssText for bulk) |
| 22 | box.style.cssText = "color: #00FF41; background: #0A0A0A; padding: 16px;"; |
| 23 | // Warning: cssText overwrites ALL inline styles |
| 24 | |
| 25 | // Better: use Object.assign for multiple properties |
| 26 | Object.assign(box.style, { |
| 27 | color: "#00FF41", |
| 28 | backgroundColor: "#0A0A0A", |
| 29 | padding: "16px", |
| 30 | borderRadius: "8px", |
| 31 | }); |
Computed Styles
| 1 | // getComputedStyle — read the FINAL applied style (from any source) |
| 2 | const box = document.querySelector(".box"); |
| 3 | const computed = getComputedStyle(box); |
| 4 | |
| 5 | console.log(computed.color); // "rgb(0, 255, 65)" (always RGB) |
| 6 | console.log(computed.fontSize); // "14px" (resolved value) |
| 7 | console.log(computed.backgroundColor); // "rgb(10, 10, 10)" |
| 8 | |
| 9 | // Read CSS custom properties via computed style |
| 10 | const brand = computed.getPropertyValue("--brand"); |
| 11 | console.log(brand); // "#00FF41" or whatever is computed |
| 12 | |
| 13 | // getComputedStyle triggers a synchronous reflow — cache if needed |
| 14 | const styles = getComputedStyle(element); |
| 15 | // Use the cached styles object multiple times |
| 16 | |
| 17 | // Practical: check if element is visible |
| 18 | function isVisible(element) { |
| 19 | const style = getComputedStyle(element); |
| 20 | return style.display !== "none" && style.visibility !== "hidden"; |
| 21 | } |
| 22 | |
| 23 | // Practical: get element dimensions |
| 24 | const rect = box.getBoundingClientRect(); |
| 25 | console.log({ |
| 26 | top: rect.top, |
| 27 | left: rect.left, |
| 28 | width: rect.width, |
| 29 | height: rect.height, |
| 30 | }); |
Class-Based Styling (Preferred)
| 1 | // Using classes is better than inline styles for maintainability |
| 2 | |
| 3 | // Add/remove classes |
| 4 | element.classList.add("is-active"); |
| 5 | element.classList.remove("is-loading"); |
| 6 | element.classList.toggle("expanded"); |
| 7 | |
| 8 | // Toggle with a boolean condition |
| 9 | element.classList.toggle("dark-mode", isDarkModeEnabled); |
| 10 | |
| 11 | // Replace one class with another |
| 12 | element.classList.replace("btn-primary", "btn-secondary"); |
| 13 | |
| 14 | // Check class existence |
| 15 | if (element.classList.contains("active")) { |
| 16 | console.log("Element is active"); |
| 17 | } |
| 18 | |
| 19 | // Practical: theme toggle |
| 20 | function toggleTheme() { |
| 21 | document.body.classList.toggle("dark-theme"); |
| 22 | const isDark = document.body.classList.contains("dark-theme"); |
| 23 | localStorage.setItem("theme", isDark ? "dark" : "light"); |
| 24 | } |
| 25 | |
| 26 | // Practical: media query listener |
| 27 | const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); |
| 28 | function handleColorScheme(e) { |
| 29 | document.body.classList.toggle("dark-theme", e.matches); |
| 30 | } |
| 31 | mediaQuery.addEventListener("change", handleColorScheme); |
best practice
DOM operations are among the most expensive operations in client-side JavaScript. Every time you read or change the DOM, the browser may need to recalculate styles, reflow layout, and repaint. Understanding performance patterns is crucial for building smooth, responsive applications.
DocumentFragment
A DocumentFragment is a lightweight container that exists outside the live DOM tree. You can build an entire subtree in a fragment and append it to the DOM in a single operation, triggering only one reflow instead of one per element.
| 1 | // Inefficient — one reflow per appendChild |
| 2 | const list = document.querySelector("ul"); |
| 3 | for (let i = 0; i < 1000; i++) { |
| 4 | const li = document.createElement("li"); |
| 5 | li.textContent = `Item ${i}`; |
| 6 | list.appendChild(li); // 1000 reflows! |
| 7 | } |
| 8 | |
| 9 | // Efficient — single reflow with DocumentFragment |
| 10 | const fragment = document.createDocumentFragment(); |
| 11 | for (let i = 0; i < 1000; i++) { |
| 12 | const li = document.createElement("li"); |
| 13 | li.textContent = `Item ${i}`; |
| 14 | fragment.appendChild(li); // no reflow — offscreen |
| 15 | } |
| 16 | list.appendChild(fragment); // single reflow |
Batch DOM Reads & Writes
Layout thrashing occurs when you interleave DOM reads and writes. Every read of a layout property (like offsetHeight or getBoundingClientRect) forces the browser to recalculate styles if there are pending writes. Batch reads together and writes together.
| 1 | // Layout thrashing — interleaving reads and writes |
| 2 | const boxes = document.querySelectorAll(".box"); |
| 3 | for (const box of boxes) { |
| 4 | const width = box.offsetWidth; // read (forces layout) |
| 5 | box.style.width = width + 10 + "px"; // write (invalidates layout) |
| 6 | } |
| 7 | // Next iteration: read forces layout recalculation again |
| 8 | |
| 9 | // Batch reads, then batch writes |
| 10 | const widths = []; |
| 11 | for (const box of boxes) { |
| 12 | widths.push(box.offsetWidth); // all reads first |
| 13 | } |
| 14 | for (let i = 0; i < boxes.length; i++) { |
| 15 | boxes[i].style.width = widths[i] + 10 + "px"; // all writes after |
| 16 | } |
| 17 | |
| 18 | // Avoid forced synchronous layouts |
| 19 | // The following properties trigger layout when read: |
| 20 | // offsetTop, offsetLeft, offsetWidth, offsetHeight |
| 21 | // clientTop, clientLeft, clientWidth, clientHeight |
| 22 | // scrollTop, scrollLeft, scrollWidth, scrollHeight |
| 23 | // getComputedStyle(), getBoundingClientRect() |
| 24 | // scrollX, scrollY, innerWidth, innerHeight |
Performance Best Practices
pro tip
Cache DOM Selections
Querying the DOM is relatively expensive. If you need to access the same element multiple times, store the reference in a variable.
| 1 | // Bad — querying the DOM repeatedly |
| 2 | document.querySelector(".header").style.color = "red"; |
| 3 | document.querySelector(".header").classList.add("highlighted"); |
| 4 | document.querySelector(".header").textContent = "New Header"; |
| 5 | |
| 6 | // Good — cache the selection |
| 7 | const header = document.querySelector(".header"); |
| 8 | header.style.color = "red"; |
| 9 | header.classList.add("highlighted"); |
| 10 | header.textContent = "New Header"; |
| 11 | |
| 12 | // Bad — querying inside a loop |
| 13 | for (let i = 0; i < 100; i++) { |
| 14 | document.querySelector(".item").innerHTML = `<span>${i}</span>`; |
| 15 | } |
| 16 | |
| 17 | // Good — query once, use reference |
| 18 | const item = document.querySelector(".item"); |
| 19 | for (let i = 0; i < 100; i++) { |
| 20 | item.innerHTML = `<span>${i}</span>`; |
| 21 | } |
| 22 | |
| 23 | // Note: if elements are dynamically added/removed, re-query as needed |
| 24 | // Stale references point to elements that may no longer be in the DOM |
Use Event Delegation
Instead of attaching event listeners to every individual element, attach a single listener to a common ancestor and use event.target to determine which child was interacted with. This uses fewer listeners and automatically handles dynamically added elements.
| 1 | // Bad — listener on every button |
| 2 | document.querySelectorAll(".delete-btn").forEach((btn) => { |
| 3 | btn.addEventListener("click", handleDelete); |
| 4 | }); |
| 5 | // New buttons added later won't have the listener |
| 6 | |
| 7 | // Good — event delegation on parent |
| 8 | document.querySelector(".list-container").addEventListener("click", (event) => { |
| 9 | const button = event.target.closest(".delete-btn"); |
| 10 | if (!button) return; // not what we're looking for |
| 11 | |
| 12 | const item = button.closest(".list-item"); |
| 13 | item.remove(); |
| 14 | }); |
| 15 | |
| 16 | // Practical: delegated click in a data table |
| 17 | document.querySelector("table").addEventListener("click", (event) => { |
| 18 | const editBtn = event.target.closest(".edit-btn"); |
| 19 | const deleteBtn = event.target.closest(".delete-btn"); |
| 20 | const checkbox = event.target.closest('input[type="checkbox"]'); |
| 21 | |
| 22 | if (editBtn) handleEdit(editBtn.dataset.id); |
| 23 | if (deleteBtn) handleDelete(deleteBtn.dataset.id); |
| 24 | if (checkbox) handleCheckbox(checkbox); |
| 25 | }); |
Security — Avoid innerHTML with User Data
| 1 | // DANGEROUS — XSS vulnerability |
| 2 | const userInput = "<img src=x onerror='alert("XSS")'>"; |
| 3 | element.innerHTML = userInput; // Executes the script! |
| 4 | |
| 5 | // SAFE — use textContent |
| 6 | element.textContent = userInput; // Escapes HTML, displays as text |
| 7 | |
| 8 | // SAFE — create elements and set properties |
| 9 | const img = document.createElement("img"); |
| 10 | img.src = userInput; // If userInput is a URL, validate it first |
| 11 | |
| 12 | // SAFE — sanitize before using innerHTML |
| 13 | function sanitizeHTML(str) { |
| 14 | const div = document.createElement("div"); |
| 15 | div.textContent = str; |
| 16 | return div.innerHTML; |
| 17 | } |
| 18 | |
| 19 | // Even better: use a DOMPurify library for complex cases |
| 20 | // import DOMPurify from "dompurify"; |
| 21 | // element.innerHTML = DOMPurify.sanitize(userInput); |
| 22 | |
| 23 | // Always validate and escape user input before inserting into DOM |
| 24 | function escapeHTML(str) { |
| 25 | const div = document.createElement("div"); |
| 26 | div.textContent = str; |
| 27 | return div.innerHTML; |
| 28 | } |
Essential DOM Guidelines
warning