LearnFrenzy
Sign in Take a free test

DOM, Events and Forms · Lesson 34 of 40

DOM Manipulation in JavaScript

6 min read Updated Nov 7, 2026 Saurabh Samir

When a browser loads HTML, it builds a tree of objects called the DOM (Document Object Model). JavaScript can read and change that tree, and the browser immediately updates the screen. Showing a notification, adding a row to a table, opening a menu – all of it is DOM manipulation. Frameworks like React do this for you, but they're built on exactly these APIs, and every front-end interview expects you to know them.

In this lesson

  • The DOM tree
  • Selecting elements with querySelector
  • Changing text, HTML, classes, attributes and styles
  • Creating, inserting and removing elements efficiently

The DOM tree

<body>
  <header>
    <h1 id="title">My Courses</h1>
  </header>
  <ul class="course-list">
    <li class="course" data-id="1">JavaScript</li>
    <li class="course" data-id="2">SQL</li>
  </ul>
</body>

The same page as a DOM tree

document → html → body

  • header→ h1#title → text "My Courses"
  • ul.course-list→ li.course (data-id=1) "JavaScript" and li.course (data-id=2) "SQL"

Every tag becomes an element node; text inside it becomes a text node. Each element object has properties (textContent, className, children…) and methods (append, remove, addEventListener…).

Selecting elements

const title = document.querySelector("#title");             // first match, or null
const firstCourse = document.querySelector(".course");
const allCourses = document.querySelectorAll(".course");    // static NodeList
const sqlItem = document.querySelector('[data-id="2"]');     // any CSS selector works
const list = title.closest("body").querySelector(".course-list");

console.log(allCourses.length);                // 2
allCourses.forEach((li) => console.log(li.textContent));
const names = [...allCourses].map((li) => li.textContent);  // NodeList → array
MethodReturns
querySelector(css)first matching element or null
querySelectorAll(css)a static NodeList of all matches (has forEach)
getElementById(id)element or null (fast, older style)
el.closest(css)nearest ancestor (or itself) that matches
el.matches(css)true/false

Common mistake: null elements

If the selector matches nothing (a typo, or the script ran before the HTML loaded), querySelector returns null and the next line throws "Cannot read properties of null". Load scripts with defer or type="module", and check optional elements: document.querySelector(".banner")?.remove().

Changing content

const title = document.querySelector("#title");
title.textContent = "My Learning Path";           // plain text – safe

const box = document.querySelector("#welcome");
box.innerHTML = "<strong>Welcome back!</strong> You have <em>3</em> lessons left.";

const userName = '<img src=x onerror="alert(1)">';     // malicious input
box.textContent = `Hi ${userName}`;                    // shown as text, never executed

textContent vs innerHTML

textContent sets plain text – any < is displayed, not parsed. innerHTML parses HTML, so inserting user input with it opens an XSS hole. Rule: use textContent for data, and only use innerHTML with trusted, fixed markup.

Classes, attributes, dataset and styles

const card = document.querySelector(".course");

card.classList.add("active");
card.classList.remove("hidden");
card.classList.toggle("expanded");              // add if missing, remove if present
card.classList.toggle("done", progress === 100); // force on/off with a condition
console.log(card.classList.contains("active")); // true

const link = document.querySelector("a.docs");
link.setAttribute("href", "/learning/javascript");
link.setAttribute("aria-current", "page");
console.log(link.getAttribute("href"));

// data-* attributes ⇄ dataset (data-course-id → dataset.courseId)
console.log(card.dataset.id);                   // "1" – always a string
card.dataset.status = "completed";              // adds data-status="completed"

card.style.setProperty("--progress", "75%");    // set a CSS variable
card.hidden = true;                             // the hidden attribute

Prefer toggling classes to setting style directly: keep the look in CSS and let JavaScript switch states. That keeps styles in one place and makes dark mode, animations and responsive design easier.

Creating and inserting elements

function createCourseCard({ id, title, lessons }) {
  const li = document.createElement("li");
  li.className = "course";
  li.dataset.id = id;

  const name = document.createElement("strong");
  name.textContent = title;                       // safe for any title

  const meta = document.createElement("span");
  meta.textContent = ` · ${lessons} lessons`;

  li.append(name, meta);
  return li;
}

const list = document.querySelector(".course-list");
list.append(createCourseCard({ id: 3, title: "React", lessons: 28 }));   // at the end
list.prepend(createCourseCard({ id: 0, title: "HTML", lessons: 12 }));   // at the start
list.querySelector('[data-id="2"]').before(createCourseCard({ id: 9, title: "Git", lessons: 8 }));
list.querySelector('[data-id="9"]').remove();

Result: the list now reads HTML, JavaScript, SQL, React.

Rendering many items efficiently

const courses = await (await fetch("/api/courses")).json();

const fragment = document.createDocumentFragment();     // an off-screen container
for (const course of courses) fragment.append(createCourseCard(course));
list.replaceChildren(fragment);                         // one update to the page

Why batch DOM updates

  1. Each insertion into the live pagecan trigger style and layout work
  2. Build in a fragment insteadno layout while you add items
  3. Insert oncereplaceChildren / append the fragment
  4. Browser lays out oncesmooth, even with hundreds of items

Templates

<template id="course-tpl">
  <li class="course"><strong class="name"></strong> <span class="meta"></span></li>
</template>
const tpl = document.querySelector("#course-tpl");
function renderCourse(course) {
  const node = tpl.content.cloneNode(true);       // copy the template's markup
  node.querySelector(".name").textContent = course.title;
  node.querySelector(".meta").textContent = `${course.lessons} lessons`;
  return node;
}

Traversing

const item = document.querySelector('[data-id="1"]');
item.parentElement;          // the <ul>
item.children;               // child elements (HTMLCollection)
item.nextElementSibling;     // the SQL <li>
item.previousElementSibling; // null – it's the first
item.closest(".course-list") // nearest matching ancestor

Practice

Try it yourself

  1. Open your browser's console on any page and run: document.querySelectorAll("a").length, then change the page heading with document.querySelector("h1").textContent = "Edited!".

  2. Write renderList(items) that turns an array of strings into <li> elements inside #todo-list, safely.

    Show solution
    function renderList(items) {
      const ul = document.querySelector("#todo-list");
      ul.replaceChildren(...items.map((text) => {
        const li = document.createElement("li");
        li.textContent = text;
        return li;
      }));
    }
    renderList(["Revise closures", "Practise SQL joins"]);

Quick check

What does querySelector return when nothing matches?

null.

Why prefer textContent for user data?

It never parses HTML, so it can't run injected scripts (XSS).

How do you read data-course-id?

el.dataset.courseId – a string.

Summary

  • The DOM is a live tree of objects; select with querySelector/querySelectorAll.
  • Use textContent for data, classList for state, dataset for data-* attributes.
  • Create with createElement, insert with append/prepend/before, batch with fragments.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Popular