JavaScript Practice #1 DOM Manipulation Basics

6 min read

The final track of the JavaScript series — Practice. Here we bring together the tools sharpened in Basics, Intermediate, and Advanced to build dynamic web pages in vanilla JavaScript.

A 6-post series.

  • #1 DOM manipulation basics ← this post
  • #2 Event handling and delegation
  • #3 fetch and async UI
  • #4 Working with forms — validation, FormData
  • #5 Local storage and lightweight state management
  • #6 Building a small app — Todo or live search

This post covers the basics of touching the DOM — finding elements, changing content/attributes/classes, creating new elements and inserting them.

What is the DOM? #

DOM (Document Object Model) is the browser’s conversion of HTML into an object tree that JavaScript can manipulate. Tags like <h1>, <p> all become JavaScript objects.

simple HTML
<body>
  <h1 id="title">hello</h1>
  <p class="message">body</p>
</body>

JavaScript sees:

DOM tree
document
  └─ html
      └─ body
          ├─ h1#title  "hello"
          └─ p.message  "body"

document is the entry point; you reach every element through the tree structure.

Finding elements — the querySelector family #

The most-used method.

finding elements
// CSS selector — single
const title = document.querySelector('#title');
const firstP = document.querySelector('p');
const greenBtn = document.querySelector('.btn.green');

// CSS selector — many (NodeList)
const allP = document.querySelectorAll('p');
const items = document.querySelectorAll('.item');

CSS selectors work directly — intuitive. In the past, getElementById, getElementsByClassName, and getElementsByTagName were used separately, but modern code uses querySelector almost exclusively.

querySelectorAll’s NodeList — not an array #

NodeList isn't an array
const items = document.querySelectorAll('.item');

items.length;       // OK — has length
items.forEach(...);  // OK — forEach works

items.map(...);      // ✗ no map
items.filter(...);   // ✗ no filter

// to a real array
const arr = [...items];           // OK — spread
const arr2 = Array.from(items);   // OK

forEach exists on NodeList, but map/filter/reduce don’t. Convert with spread or Array.from when you need array methods.

Parent/child/sibling — tree traversal #

tree traversal
const el = document.querySelector('.item');

// parent
el.parentElement;
el.parentNode;       // nearly identical (edge cases differ)

// children
el.children;          // HTMLCollection — elements only
el.childNodes;        // NodeList — includes text/comments
el.firstElementChild;
el.lastElementChild;

// siblings
el.nextElementSibling;
el.previousElementSibling;

// closest ancestor (including self)
el.closest('.container');

closest is used often — the heart of the event delegation pattern (#2).

Changing content — textContent vs innerHTML #

text vs HTML
const el = document.querySelector('#title');

// text only — safe
el.textContent = 'new title';

// parsed as HTML — can be dangerous
el.innerHTML = '<strong>new title</strong>';

textContent inserts plain text only. Safe even with user input or external data.

innerHTML parses as HTML. The <strong> tag really renders as bold, but inserting untrusted data leaves you vulnerable to XSS.

XSS risk
const userInput = '<img src=x onerror="alert(1)">';

el.textContent = userInput;   // safe — just a string
el.innerHTML = userInput;     // ✗ script executes

Default to textContent. When you really need HTML, use innerHTML carefully and only there.

Working with attributes #

Regular attributes #

attributes
const link = document.querySelector('a');

link.href;                          // direct property access
link.href = 'https://...';
link.getAttribute('href');           // same
link.setAttribute('href', 'https://...');
link.removeAttribute('href');

link.hasAttribute('target');         // true/false

For common attributes like href, src, id — direct dot access is shorter. Less-known attributes or dynamic keys — getAttribute/setAttribute.

Data attributes — data-* #

For embedding JavaScript-side data in HTML.

HTML
<button data-id="42" data-action="delete">delete</button>
access via dataset
const btn = document.querySelector('button');

btn.dataset.id;        // '42' (always string)
btn.dataset.action;    // 'delete'

btn.dataset.id = '100';   // updates attribute → HTML changes

Mind camelCase conversion — HTML’s data-user-name becomes dataset.userName in JS.

Working with classes — classList #

classList
const el = document.querySelector('.box');

el.classList.add('active');
el.classList.remove('hidden');
el.classList.toggle('open');                 // remove if present, add if not
el.classList.toggle('disabled', isDisabled); // force with the second arg

el.classList.contains('active');   // boolean
el.classList.replace('a', 'b');

classList is the modern standard tool. Safer and more readable than the old el.className = 'a b c' (string-level) approach.

Working with styles #

Inline styles #

style property
const el = document.querySelector('.box');

el.style.color = 'red';
el.style.backgroundColor = '#eee';   // CSS background-color is camelCase in JS
el.style.transform = 'translateX(10px)';

But don’t overuse inline styles. Toggling a CSS class is almost always cleaner.

recommended — toggle a class
el.classList.add('error');
// CSS defines .error { color: red; ... }

CSS variables #

CSS custom properties (--var) can be set dynamically with setProperty.

CSS variables
el.style.setProperty('--main-color', '#ff5733');
const value = getComputedStyle(el).getPropertyValue('--main-color');

Fits theme switching and dynamic styling.

Creating and inserting elements #

Create a new element #

createElement
const div = document.createElement('div');
div.className = 'item';
div.textContent = 'new item';

Inserting — append / prepend / before / after #

insertion methods
const parent = document.querySelector('.list');
const item = document.createElement('li');

parent.append(item);                 // append to end
parent.prepend(item);                // prepend to start
existingChild.before(item);          // before existing child
existingChild.after(item);           // after existing child
existingChild.replaceWith(item);     // replace child

The old methods (appendChild, insertBefore) work too — but the new ones are shorter and clearer, so modern code uses these.

Many elements at once #

many children at once
parent.append(li1, li2, li3);
parent.append('text works too', divEl, '\n');

It accepts strings too, auto-converting them to text nodes.

Removing — remove #

remove self
el.remove();

Much shorter than the old el.parentNode.removeChild(el).

Efficiency — DocumentFragment #

Adding many elements one by one triggers reflow each insertion — expensive. DocumentFragment acts as a temporary container.

batch many children
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.append(li);   // gather in fragment first
}

list.append(fragment);   // attach to the real tree once — single reflow

Differences become visible around 100 items. Most cases don’t need this — but worth knowing for large lists.

innerHTML + template literals — short but careful #

innerHTML in one shot
list.innerHTML = items
  .map((item) => `<li class="item">${item.name}</li>`)
  .join('');

Very short — but if ${item.name} is user input, you have an XSS risk. Use only for trusted data, or after escaping.

Element info #

commonly used info
el.tagName;              // 'DIV' (uppercase)
el.id;
el.className;            // string — prefer classList

el.getBoundingClientRect();   // on-screen position/size
el.offsetWidth;
el.offsetHeight;
el.offsetTop;
el.offsetLeft;

el.scrollTop;
el.scrollLeft;

Comes up often when handling layout or scrolling.

Using closest #

The method we touched on in tree traversal. It comes up so often in event handling that it’s worth a second mention.

closest pattern
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (!item) return;

  const id = item.dataset.id;
  // ...
});

Click anywhere inside the list — find the nearest .item and pull id. Foundation of the event delegation pattern we cover next.

Wrap-up #

What we covered:

  • DOM is the JavaScript interface that turns HTML into an object tree
  • querySelector / querySelectorAll are the modern standard
  • NodeList isn’t an array — use spread/Array.from when you need array methods
  • textContent is safe; use innerHTML only with trusted data
  • Manage data-* attributes with dataset
  • classList.add/remove/toggle are the class standard
  • append/prepend/before/after are modern insertion
  • For big lists, batch with DocumentFragment
  • closest for the nearest ancestor

In the next post (#2 Event Handling and Delegation) we cover the event object, bubbling and capturing, and efficient event-delegation patterns.

X