JavaScript DOM Manipulation | Control Web Pages Dynamically

Key takeaways

JavaScript DOM tutorial: select and update elements, events, delegation, forms, and common pitfalls—querySelector, addEventListener, and production-ready patterns.

Introduction

What is the DOM?

The DOM (Document Object Model) represents an HTML document as a tree. JavaScript can manipulate the DOM to change the page dynamically.

<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
  </head>
  <body>
    <h1 id="title">Hello</h1>
    <p class="content">Some content</p>
  </body>
</html>

DOM tree:

document
  └─ html
      ├─ head
      │   └─ title
      └─ body
          ├─ h1#title
          └─ p.content

A closer look at the tree

  • document: Entry to the tree. document.documentElement is <html>; document.body is <body>.
  • Node: Not only elements—text and comments are nodes too. In <p>Hi</p>, "Hi" is a text node.
  • Parent, child, sibling: Navigate with parentElement, children, nextElementSibling, etc. (see «Other selection helpers» below).
  • Rendering: The browser parses HTML into a DOM and paints with CSS. Changing the DOM (text, attributes, children) updates what you see.

This distinction between “the HTML you wrote” and “the DOM the browser built from it” is worth being precise about, because it’s the source of a lot of beginner confusion: the DOM is a live, in-memory object tree the browser constructs by parsing your HTML — it is not the HTML source text itself, and it doesn’t stay in sync with it. Once JavaScript changes an element’s text or adds a new child, the original HTML source is completely irrelevant; the browser renders whatever the current DOM tree looks like at that moment. This is also why “view page source” in a browser (which shows the original HTML) and the DevTools Elements panel (which shows the live, currently-mutated DOM) can show two different things for the same page once JavaScript has run — the Elements panel is the one that reflects reality.

In practice, use the Elements panel in DevTools to inspect structure often.


Selecting elements

getElementById

// By id (often fastest)
const title = document.getElementById("title");
console.log(title.textContent);  // Hello

getElementById is a genuinely different, older API than the querySelector family below it — it doesn’t take a CSS selector at all, just a bare id string with no # prefix, and it’s implemented as a direct lookup the browser can optimize heavily since ids are supposed to be unique per document. document.querySelector("#title") returns the same element but goes through the more general CSS-selector-matching machinery, which is more flexible but has more work to do. For a large page with many id lookups in a hot path, that difference can be measurable; for typical UI code the readability of querySelector’s consistent CSS-selector syntax usually wins.

querySelector / querySelectorAll

// CSS selector (first match)
const title = document.querySelector("#title");
const content = document.querySelector(".content");
const firstP = document.querySelector("p");
// All matches
const allPs = document.querySelectorAll("p");
console.log(allPs.length);  // number of p elements
// Complex selectors
const link = document.querySelector("div.container > a.link");
const items = document.querySelectorAll("ul li:nth-child(odd)");
// Iterate NodeList
allPs.forEach(p => {
    console.log(p.textContent);
});

querySelectorAll returns a NodeList, not a real array — it happens to support .forEach() (a deliberate, later addition to the spec specifically because people kept wanting it), but it doesn’t have .map(), .filter(), or .reduce() the way a genuine Array does. Calling Array.from(document.querySelectorAll(...)) first converts it into a real array whenever you need one of those other methods, which is a common enough need that it’s worth remembering as a one-line fix rather than being surprised by a TypeError: allPs.map is not a function.

Other selection helpers

// By class
const elements = document.getElementsByClassName("content");
// By tag
const paragraphs = document.getElementsByTagName("p");
// Children
const parent = document.getElementById("parent");
const children = parent.children;  // HTMLCollection
const firstChild = parent.firstElementChild;
const lastChild = parent.lastElementChild;
// Siblings
const element = document.getElementById("myElement");
const next = element.nextElementSibling;
const prev = element.previousElementSibling;
// Parent
const parent = element.parentElement;

children and parent.childNodes look interchangeable but return different things — children (an HTMLCollection) only includes element nodes, while childNodes (a NodeList) includes text nodes and comment nodes too, which means whitespace between tags in your HTML source shows up as text nodes in childNodes but never in children. This is exactly why firstElementChild/lastElementChild exist alongside the older firstChild/lastChild — the “Element” versions skip whitespace text nodes, which is almost always what you actually want when navigating structure rather than text content.


Changing elements

Text

const title = document.getElementById("title");
// textContent: plain text
title.textContent = "New Title";
// innerHTML: parses HTML (watch XSS!)
title.innerHTML = "New <strong>Title</strong>";
// innerText: visible text (layout-aware)
title.innerText = "Title";

innerText looks like a synonym for textContent but behaves meaningfully differently: innerText is aware of CSS and rendering — it won’t return the text of an element hidden with display: none, and it collapses whitespace the way visible text actually renders. textContent returns the raw text content regardless of styling, including hidden elements, with no rendering-aware normalization. That distinction also has a performance cost: reading innerText can force the browser to recalculate layout (a “reflow”) to know what’s actually visible, which textContent never needs to do — for code that reads element text frequently, textContent is both the more predictable and the cheaper choice.

innerHTML vs textContent in production

textContentinnerHTML
ContentPlain text onlyParses HTML strings into the DOM
XSSUser input won’t execute as markupUntrusted strings can inject scripts
CostSimple and safeHTML parse cost + security concerns

Rule: For user input or API text, default to textContent. If you truly need HTML, sanitize first (e.g. DOMPurify). For dynamic lists, createElement + appendChild is often easier to audit than big innerHTML strings.

// ✅ Safe: text only
el.textContent = userInput;
// ⚠️ Risky: userInput could contain <script>
el.innerHTML = userInput;

Attributes

const link = document.querySelector("a");
// Read
console.log(link.getAttribute("href"));
// Write
link.setAttribute("href", "https://google.com");
link.setAttribute("target", "_blank");
// Remove
link.removeAttribute("target");
// Direct properties
link.href = "https://google.com";
link.id = "myLink";
link.className = "link active";
// classList
link.classList.add("highlight");
link.classList.remove("active");
link.classList.toggle("selected");  // remove if present, else add
console.log(link.classList.contains("highlight"));  // true

Styles

const box = document.getElementById("box");
// Inline styles
box.style.color = "red";
box.style.backgroundColor = "yellow";
box.style.fontSize = "20px";
// Multiple at once
Object.assign(box.style, {
    color: "blue",
    backgroundColor: "lightgray",
    padding: "10px",
    borderRadius: "5px"
});
// Computed styles
const styles = window.getComputedStyle(box);
console.log(styles.color);  // rgb(0, 0, 255)

Creating and removing elements

Creating

// Create
const div = document.createElement("div");
div.textContent = "New element";
div.className = "box";
div.id = "newBox";
div.setAttribute("data-id", "123");
// Append
document.body.appendChild(div);  // at end of body
// Insert at a position
const container = document.getElementById("container");
const firstChild = container.firstElementChild;
container.insertBefore(div, firstChild);  // before first child
// insertAdjacentHTML
container.insertAdjacentHTML("beforeend", "<p>New paragraph</p>");
// beforebegin: before element
// afterbegin: before first child
// beforeend: after last child
// afterend: after element

An element created with createElement exists only in memory until it’s actually attached somewhere — document.createElement("div") doesn’t touch the visible page at all, which is what makes it safe to build up a whole subtree (setting attributes, adding children) before a single appendChild call puts the finished structure into the live document in one step, rather than triggering a separate re-render for each intermediate change.

Removing

const element = document.getElementById("myElement");
// Option 1: remove()
element.remove();
// Option 2: removeChild()
const parent = element.parentElement;
parent.removeChild(element);
// Clear all children
const container = document.getElementById("container");
container.innerHTML = "";  // simple but does not remove listeners cleanly
// Or
while (container.firstChild) {
    container.removeChild(container.firstChild);
}

The “does not remove listeners cleanly” comment on innerHTML = "" deserves unpacking, because the actual behavior is more specific than “listeners leak forever.” Setting innerHTML destroys the old child elements and lets the garbage collector reclaim them (and their listeners) once nothing else references them — that part is fine. The real trap is any external reference your own code kept to one of those now-discarded elements: if you’d stored const btn = container.querySelector("button") earlier and later call container.innerHTML = "", btn still points at a now-detached DOM node whose listeners still technically fire, but clicking it does nothing visible since it’s no longer part of the page — a subtle, hard-to-spot bug distinct from a “real” memory leak.

Cloning

const original = document.getElementById("original");
// Shallow (no descendants)
const shallowClone = original.cloneNode(false);
// Deep (with descendants)
const deepClone = original.cloneNode(true);
document.body.appendChild(deepClone);

One gotcha with cloneNode that surprises people the first time: it never clones event listeners attached via addEventListener, regardless of whether you pass true or false. “Deep” here refers only to the DOM structure (descendant elements and their attributes) — a cloned button that had a click listener attached in JavaScript will render identically but do nothing when clicked, until you attach a new listener to the clone yourself.


Events

Listeners

const button = document.getElementById("myButton");
button.addEventListener("click", function(event) {
    console.log("Clicked!");
    console.log("Event type:", event.type);
    console.log("Target:", event.target);
});
// Arrow function
button.addEventListener("click", (e) => {
    console.log("Clicked!");
});
// Remove listener (same reference)
function handleClick(e) {
    console.log("Clicked!");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);
// Run once
button.addEventListener("click", () => {
    console.log("Runs only once");
}, { once: true });

removeEventListener only works when passed the exact same function reference that was registered — button.addEventListener("click", () => {...}) followed by button.removeEventListener("click", () => {...}) does nothing, because those are two distinct anonymous function objects even though they look identical in source code. This is precisely why handleClick here is defined as a named function and stored in a variable rather than passed inline: it’s the only way to keep a reference around that both addEventListener and the later removeEventListener call can share.

Propagation: capturing and bubbling

Events travel the DOM in two phases:

  1. Capturing: window → target (top down).
  2. Target: the element that received the event.
  3. Bubbling: target → window (bottom up). Most events bubble. The third argument to addEventListener controls the capture phase.
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
outer.addEventListener(
  "click",
  () => console.log("outer capture"),
  { capture: true }
);
inner.addEventListener("click", () => console.log("inner target"));
outer.addEventListener("click", () => console.log("outer bubble"));
// Click inner: typical order — outer capture → inner target → outer bubble

Notice outer has two listeners registered for the same event, one with { capture: true } and one without — these are not redundant, they fire at two completely different points in the event’s journey (once on the way down, once on the way back up), which is exactly why the logged order interleaves outer around inner instead of grouping by element. Capturing listeners are the less commonly used of the two phases in application code, but they matter for cases where you need to intercept an event before a child element’s own handler has a chance to act on it — for example, a modal overlay that wants to block all clicks passing through to elements beneath it during an animation.

  • event.target: the innermost element that originated the event.
  • event.currentTarget: the element the handler is bound to (may be a parent when delegating). Use event.stopPropagation() only when needed—it blocks parent handlers too.

Common events

// Mouse
element.addEventListener("click", e => {});      // click
element.addEventListener("dblclick", e => {});   // double-click
element.addEventListener("mouseenter", e => {}); // enter
element.addEventListener("mouseleave", e => {}); // leave
element.addEventListener("mousemove", e => {});  // move
// Keyboard
input.addEventListener("keydown", e => {
    console.log("Key:", e.key);
    console.log("Code:", e.code);
    
    if (e.key === "Enter") {
        console.log("Enter pressed!");
    }
});
input.addEventListener("keyup", e => {});
input.addEventListener("keypress", e => {});  // deprecated
// Forms
form.addEventListener("submit", e => {
    e.preventDefault();  // block default submit
    console.log("Form submitted");
});
input.addEventListener("input", e => {
    console.log("Input value:", e.target.value);
});
input.addEventListener("change", e => {
    console.log("Changed:", e.target.value);
});
// Window
window.addEventListener("load", () => {
    console.log("Page fully loaded");
});
window.addEventListener("resize", () => {
    console.log("Window size:", window.innerWidth, window.innerHeight);
});
window.addEventListener("scroll", () => {
    console.log("Scroll position:", window.scrollY);
});

input and change look interchangeable for a text field but fire at genuinely different times: input fires on every single keystroke or paste, immediately, while change only fires once the field loses focus (or, for a <select>, once a new option is chosen) and its value has actually changed since it gained focus. Use input for anything that should react live as the user types (character counters, live validation feedback, search-as-you-type); use change for anything that should only run once the user has finished editing, since firing on every keystroke would be wasteful or premature.

Event object

button.addEventListener("click", (event) => {
    console.log(event.type);  // click
    
    console.log(event.target);       // clicked element
    console.log(event.currentTarget); // element with the listener
    
    console.log(event.clientX, event.clientY);  // viewport
    console.log(event.pageX, event.pageY);      // document
    
    event.preventDefault();
    event.stopPropagation();
});

Event delegation

// ❌ Listener per row (wasteful for many items)
const items = document.querySelectorAll(".item");
items.forEach(item => {
    item.addEventListener("click", () => {
        console.log("Clicked:", item.textContent);
    });
});
// ✅ One listener on parent
const list = document.getElementById("list");
list.addEventListener("click", (e) => {
    if (e.target.classList.contains("item")) {
        console.log("Clicked:", e.target.textContent);
    }
});
// Works for new nodes too
const newItem = document.createElement("li");
newItem.className = "item";
newItem.textContent = "New item";
list.appendChild(newItem);

The “works for new nodes too” comment is the actual killer feature of delegation, not just a memory optimization. The per-item version attaches listeners to whatever elements exist at the moment that code runs — any .item added later, like newItem here, has no listener at all unless you remember to re-attach one every time you add a row. The delegated version needs no such bookkeeping: the single listener lives on list, which never changes, and it inspects e.target fresh on every click, so a brand-new item appended a minute later is handled correctly without any extra code. This is exactly why delegation is the standard pattern for any list where items get added or removed dynamically, and it’s what makes the to-do list example later in this article work without re-binding listeners on every add/remove.


Practical examples

Example 1: To-do list

<!DOCTYPE html>
<html>
<head>
    <title>To-Do List</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 500px;
            margin: 50px auto;
        }
        .todo-item {
            padding: 10px;
            margin: 5px 0;
            border: 1px solid #ddd;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .completed {
            text-decoration: line-through;
            opacity: 0.6;
        }
        button {
            padding: 5px 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <h1>To-Do List</h1>
    
    <input type="text" id="todoInput" placeholder="Enter a task">
    <button id="addBtn">Add</button>
    
    <div id="todoList"></div>
    
    <script>
        const input = document.getElementById("todoInput");
        const addBtn = document.getElementById("addBtn");
        const todoList = document.getElementById("todoList");
        
        // Add todo
        function addTodo() {
            const text = input.value.trim();
            
            if (!text) {
                alert("Please enter a task!");
                return;
            }
            
            const todoItem = document.createElement("div");
            todoItem.className = "todo-item";
            
            const span = document.createElement("span");
            span.textContent = text;
            
            const deleteBtn = document.createElement("button");
            deleteBtn.textContent = "Delete";
            
            todoItem.appendChild(span);
            todoItem.appendChild(deleteBtn);
            todoList.appendChild(todoItem);
            
            input.value = "";
            input.focus();
        }
        
        addBtn.addEventListener("click", addTodo);
        
        input.addEventListener("keypress", (e) => {
            if (e.key === "Enter") {
                addTodo();
            }
        });
        
        // Delegation: complete / delete
        todoList.addEventListener("click", (e) => {
            const todoItem = e.target.closest(".todo-item");
            
            if (e.target.tagName === "SPAN") {
                todoItem.classList.toggle("completed");
            } else if (e.target.tagName === "BUTTON") {
                todoItem.remove();
            }
        });
    </script>
</body>
</html>

Example 2: Tabs

<!DOCTYPE html>
<html>
<head>
    <title>Tabs UI</title>
    <style>
        .tabs {
            display: flex;
            border-bottom: 2px solid #ddd;
        }
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border: none;
            background: none;
        }
        .tab.active {
            border-bottom: 3px solid #007bff;
            color: #007bff;
        }
        .tab-content {
            padding: 20px;
            display: none;
        }
        .tab-content.active {
            display: block;
        }
    </style>
</head>
<body>
    <div class="tabs">
        <button class="tab active" data-tab="tab1">Tab 1</button>
        <button class="tab" data-tab="tab2">Tab 2</button>
        <button class="tab" data-tab="tab3">Tab 3</button>
    </div>
    
    <div id="tab1" class="tab-content active">
        <h2>Tab 1 Content</h2>
        <p>This is the first tab.</p>
    </div>
    
    <div id="tab2" class="tab-content">
        <h2>Tab 2 Content</h2>
        <p>This is the second tab.</p>
    </div>
    
    <div id="tab3" class="tab-content">
        <h2>Tab 3 Content</h2>
        <p>This is the third tab.</p>
    </div>
    
    <script>
        const tabs = document.querySelectorAll(".tab");
        const contents = document.querySelectorAll(".tab-content");
        
        tabs.forEach(tab => {
            tab.addEventListener("click", () => {
                tabs.forEach(t => t.classList.remove("active"));
                contents.forEach(c => c.classList.remove("active"));
                
                tab.classList.add("active");
                const targetId = tab.getAttribute("data-tab");
                document.getElementById(targetId).classList.add("active");
            });
        });
    </script>
</body>
</html>

Example 3: Modal

<!DOCTYPE html>
<html>
<head>
    <title>Modal</title>
    <style>
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            justify-content: center;
            align-items: center;
        }
        .modal.active {
            display: flex;
        }
        .modal-content {
            background: white;
            padding: 30px;
            border-radius: 10px;
            max-width: 500px;
        }
        .close {
            float: right;
            cursor: pointer;
            font-size: 24px;
        }
    </style>
</head>
<body>
    <button id="openModal">Open Modal</button>
    
    <div id="modal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <h2>Modal Title</h2>
            <p>This is the modal content.</p>
        </div>
    </div>
    
    <script>
        const openBtn = document.getElementById("openModal");
        const modal = document.getElementById("modal");
        const closeBtn = document.querySelector(".close");
        
        openBtn.addEventListener("click", () => {
            modal.classList.add("active");
        });
        
        closeBtn.addEventListener("click", () => {
            modal.classList.remove("active");
        });
        
        modal.addEventListener("click", (e) => {
            if (e.target === modal) {
                modal.classList.remove("active");
            }
        });
        
        document.addEventListener("keydown", (e) => {
            if (e.key === "Escape" && modal.classList.contains("active")) {
                modal.classList.remove("active");
            }
        });
    </script>
</body>
</html>

Example 4: Dynamic list (add/delete)

Build <li> nodes from input; attach delete with createElement or use delegation. Below uses delegation only.

<ul id="itemList"></ul>
<input type="text" id="itemInput" placeholder="Item">
<button type="button" id="addBtn">Add</button>
<script>
  const list = document.getElementById("itemList");
  const input = document.getElementById("itemInput");
  const addBtn = document.getElementById("addBtn");
  function addItem() {
    const text = input.value.trim();
    if (!text) return;
    const li = document.createElement("li");
    li.textContent = text; // text only (avoid HTML injection)
    const del = document.createElement("button");
    del.type = "button";
    del.textContent = "Delete";
    del.dataset.action = "delete";
    li.appendChild(del);
    list.appendChild(li);
    input.value = "";
  }
  addBtn.addEventListener("click", addItem);
  list.addEventListener("click", (e) => {
    const btn = e.target.closest("button[data-action='delete']");
    if (!btn) return;
    btn.closest("li")?.remove();
  });
</script>

Example 5: Client-side form validation

Use HTML5 required, pattern, type=“email” for first-line checks; JavaScript can show messages and manage focus. Always validate again on the server.

<form id="signup" novalidate>
  <label>
    Email
    <input type="email" id="email" name="email" required>
  </label>
  <span id="emailError" class="error" aria-live="polite"></span>
  <label>
    Password (8+ characters)
    <input type="password" id="password" name="password" minlength="8" required>
  </label>
  <span id="pwError" class="error" aria-live="polite"></span>
  <button type="submit">Sign Up</button>
</form>
<script>
  const form = document.getElementById("signup");
  const email = document.getElementById("email");
  const password = document.getElementById("password");
  const emailError = document.getElementById("emailError");
  const pwError = document.getElementById("pwError");
  function validateEmailField() {
    emailError.textContent = "";
    if (!email.validity.valid) {
      emailError.textContent = email.validationMessage || "Please check the email format.";
      return false;
    }
    return true;
  }
  function validatePasswordField() {
    pwError.textContent = "";
    if (password.value.length < 8) {
      pwError.textContent = "Password must be at least 8 characters.";
      return false;
    }
    return true;
  }
  email.addEventListener("blur", validateEmailField);
  password.addEventListener("blur", validatePasswordField);
  form.addEventListener("submit", (e) => {
    const okEmail = validateEmailField();
    const okPw = validatePasswordField();
    if (!okEmail || !okPw) {
      e.preventDefault();
      return;
    }
    console.log({ email: email.value });
  });
</script>

Forms

Form events

<form id="myForm">
    <input type="text" id="username" name="username" required>
    <input type="email" id="email" name="email" required>
    <input type="password" id="password" name="password" required>
    <button type="submit">Submit</button>
</form>
<script>
    const form = document.getElementById("myForm");
    
    form.addEventListener("submit", (e) => {
        e.preventDefault();  // avoid full page reload
        
        const formData = new FormData(form);
        
        const data = {
            username: formData.get("username"),
            email: formData.get("email"),
            password: formData.get("password")
        };
        
        console.log(data);
        
        const username = document.getElementById("username").value;
        const email = document.getElementById("email").value;
        
        if (username.length < 3) {
            alert("Username must be at least 3 characters");
            return;
        }
        
        fetch("/api/register", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(data)
        })
        .then(response => response.json())
        .then(result => console.log(result))
        .catch(error => console.error(error));
    });
    
    const username = document.getElementById("username");
    
    username.addEventListener("input", (e) => {
        const value = e.target.value;
        
        if (value.length < 3) {
            username.style.borderColor = "red";
        } else {
            username.style.borderColor = "green";
        }
    });
</script>

Common mistakes

Mistake 1: touching the DOM before it exists

// ❌ Runs before HTML is parsed
const button = document.getElementById("myButton");  // null!
button.addEventListener("click", () => {});  // TypeError
// ✅ Wait for DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
    const button = document.getElementById("myButton");
    button.addEventListener("click", () => {
        console.log("Clicked!");
    });
});
// ✅ Or put <script> at the end of <body>

Mistake 2: clearing innerHTML drops listeners

// ❌ innerHTML rebuilds subtree — listeners are gone
const container = document.getElementById("container");
const button = document.createElement("button");
button.textContent = "Click";
button.addEventListener("click", () => console.log("Clicked!"));
container.appendChild(button);
container.innerHTML = "";  // listeners removed
// ✅ removeChild loop
while (container.firstChild) {
    container.removeChild(container.firstChild);
}

Mistake 3: event bubbling surprises

// Bubbling example
<div id="parent">
    <button id="child">Button</button>
</div>
document.getElementById("parent").addEventListener("click", () => {
    console.log("Parent clicked");
});
document.getElementById("child").addEventListener("click", (e) => {
    console.log("Child clicked");
    // e.stopPropagation();
});
// Click button:
// Child clicked
// Parent clicked (bubble)

Practice problems

Problem 1: Counter

Build increment/decrement buttons around a number.

<div id="counter">
    <button id="decrease">-</button>
    <span id="count">0</span>
    <button id="increase">+</button>
</div>
<script>
    let count = 0;
    const countSpan = document.getElementById("count");
    
    document.getElementById("increase").addEventListener("click", () => {
        count++;
        countSpan.textContent = count;
    });
    
    document.getElementById("decrease").addEventListener("click", () => {
        count--;
        countSpan.textContent = count;
    });
</script>

Problem 2: Dynamic list

Add items from input and delete them.

<input type="text" id="itemInput">
<button id="addItem">Add</button>
<ul id="itemList"></ul>
<script>
    const input = document.getElementById("itemInput");
    const addBtn = document.getElementById("addItem");
    const list = document.getElementById("itemList");
    
    function addItem() {
        const text = input.value.trim();
        if (!text) return;
        
        const li = document.createElement("li");
        li.innerHTML = `
            ${text}
            <button class="delete">Delete</button>
        `;
        
        list.appendChild(li);
        input.value = "";
    }
    
    addBtn.addEventListener("click", addItem);
    
    input.addEventListener("keypress", (e) => {
        if (e.key === "Enter") addItem();
    });
    
    list.addEventListener("click", (e) => {
        if (e.target.classList.contains("delete")) {
            e.target.parentElement.remove();
        }
    });
</script>