Skip to main content

Command Palette

Search for a command to run...

HTMLCollection vs NodeList: Understanding JavaScript's DOM Collections

Understand HTMLCollection vs NodeList in detail

Updated
11 min read
HTMLCollection vs NodeList: Understanding JavaScript's DOM Collections
A

Hi, I’m Abdul Samad. A web development learner and tech enthusiast. I write about what I learn, share practical coding tips, and publish in-depth blogs on programming and modern web development.

Check out my full collection of blogs on Hashnode: https://abdulsamad30.hashnode.dev/

Connect with me on X for quick updates and insights: @abdul_sama60108

When you start working with DOM manipulation in JavaScript, you'll quickly run into two similar-looking objects: HTMLCollection and NodeList.

They look almost identical, both give you collections of elements, but they behave differently in ways that can break your code if you don't understand them.

This guide will take you from the basics of how the DOM works to understanding exactly when and why these collections behave differently.


Why Understanding DOM Collections Matters

If you've ever written code like this and wondered why it didn't work:

const items = document.getElementsByClassName("item");
items.forEach(item => console.log(item)); // ERROR

Or experienced this wierd bug:

const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
  items[i].remove(); // only removes every other element?!
}

Then you've hit the HTMLCollection vs NodeList confusion.

These aren't just different names for the same thing they have fundamentally different behaviors that affect how you write DOM code.

Understanding the difference helps you:

  • Avoid subtle bugs in loops and iterations

  • Choose the right DOM methods for your use case

  • Write more predictable and maintainable code


Understanding the DOM Tree

Before we dive into collections, you need to understand what the DOM actually is.

When a browser loads your HTML, it creates a tree structure called the Document Object Model (DOM).

Every piece of your HTML becomes a node in this tree.

Nodes vs Elements

Here's the key distinction:

Nodes are everything in the DOM tree:

  • Element nodes (<div>, <p>, <span>)

  • Text nodes (the actual text content)

  • Comment nodes (<!-- comments -->)

  • Document node (the root)

Elements are a specific type of node just the HTML tags.

<div>
  Hello World
  <span>!</span>
</div>

In this example:

  • The <div> is an element node

  • "Hello World" is a text node

  • The <span> is an element node

  • "!" is a text node

This distinction is why HTMLCollection and NodeList exist they're designed to handle these different types.


HTMLCollection: Live Collections of Elements

Now that you understand the DOM structure, let's look at HTMLCollection the older, simpler collection type.

What HTMLCollection Is

HTMLCollection is a live collection that contains only HTML element nodes. When you use methods like getElementsByClassName() or getElementsByTagName(), you get an HTMLCollection.

const boxes = document.getElementsByClassName("box");
console.log(boxes); // HTMLCollection(3) [div.box, div.box, div.box]

The "Live" Behavior

This is the most important characteristic of HTMLCollection. "Live" means the collection automatically updates when the DOM changes.

const boxes = document.getElementsByClassName("box");
console.log(boxes.length); // 3

// Add a new element with class "box"
const newBox = document.createElement("div");
newBox.className = "box";
document.body.appendChild(newBox);

console.log(boxes.length); // 4 - automatically updated!

You didn't reassign boxes or call any update method. The collection just reflects the current state of the DOM. This happens because HTMLCollection doesn't store the elements it stores a query that runs every time you access it.

Why It Only Contains Elements

HTMLCollection is designed for element manipulation. It filters out text nodes, comments, and other node types automatically:

<div class="container">
  Some text
  <p class="item">Paragraph</p>
  <!-- comment -->
  <span class="item">Span</span>
</div>
const items = document.getElementsByClassName("item");
// Only gets <p> and <span>, ignores text and comments

How to Get HTMLCollection

Three main methods return HTMLCollection:

// By class name
const byClass = document.getElementsByClassName("box");

// By tag name
const allDivs = document.getElementsByTagName("div");

// By name attribute (mostly for forms)
const inputs = document.getElementsByName("username");

// Also: element.children property
const childElements = document.body.children;

Working with HTMLCollection

HTMLCollection is array-like but not a real array:

const boxes = document.getElementsByClassName("box");

// Array-like features that work:
boxes[0]           // access by index ✓
boxes.length       // length property ✓
boxes.item(0)      // item() method ✓

// Array methods that DON'T work:
boxes.forEach()    // ✗ TypeError
boxes.map()        // ✗ TypeError
boxes.filter()     // ✗ TypeError

You can loop through it with traditional for loops:

for (let i = 0; i < boxes.length; i++) {
  console.log(boxes[i]);
}

NodeList: Collections of Any Node Type

NodeList is more flexible than HTMLCollection. It can contain any type of node, and depending on how you get it, it might be live or static.

What NodeList Is

NodeList represents a collection of nodes not just elements. The most common way to get one is through querySelectorAll():

const items = document.querySelectorAll(".item");
console.log(items); // NodeList(2) [div.item, p.item]

Static vs Live NodeList

Here's where it gets interesting most NodeLists are static, but not all.

Static NodeList (from querySelectorAll):

const items = document.querySelectorAll(".item");
console.log(items.length); // 2

// Add a new .item element
const newItem = document.createElement("div");
newItem.className = "item";
document.body.appendChild(newItem);

console.log(items.length); // Still 2 - doesn't update

The NodeList is a snapshot taken at the moment you called querySelectorAll(). DOM changes after that don't affect it.

Live NodeList (from childNodes):

const children = document.body.childNodes;
console.log(children.length); // 5

// Add a new child
document.body.appendChild(document.createElement("div"));

console.log(children.length); // 6 - updates automatically

The childNodes property returns a live NodeList that includes all node types (elements, text, comments).

NodeList Has forEach

Unlike HTMLCollection, NodeList includes the forEach() method:

const items = document.querySelectorAll(".item");

items.forEach(item => {
  console.log(item.textContent);
}); // Works perfectly

But it's still not a full array no map(), filter(), reduce(), etc.

How to Get NodeList

// Static NodeList - most common
const selected = document.querySelectorAll(".item");

// Live NodeList - includes all node types
const allChildren = element.childNodes;

// Note: querySelector returns a single element, not a NodeList
const single = document.querySelector(".item"); // just an Element

Live vs Static: The Difference

This is where most bugs happen. Let's see exactly why live collections can break your code.

The Live Collection Problem

const boxes = document.getElementsByClassName("box");

// Dangerous: infinite loop risk
for (let i = 0; i < boxes.length; i++) {
  const clone = boxes[i].cloneNode(true);
  document.body.appendChild(clone);
  // boxes.length keeps increasing!
}

Every time you append a cloned .box, it's added to the collection while the loop is running. The length keeps growing and the loop never ends.

Another common bug:

const items = document.getElementsByClassName("item");

// Trying to remove all items
for (let i = 0; i < items.length; i++) {
  items[i].remove();
}
// Only removes every other item!

Why? When you remove items[0], the collection immediately updates. What was items[1] becomes items[0]. Then the loop moves to i = 1, skipping what is now at index 0.

The Static Collection Solution

const boxes = document.querySelectorAll(".box");

// Safe: collection length is fixed
for (let i = 0; i < boxes.length; i++) {
  const clone = boxes[i].cloneNode(true);
  document.body.appendChild(clone);
  // boxes.length stays the same
}

The static NodeList doesn't change when you modify the DOM. You're working with a snapshot, so iterations are predictable.


HTMLCollection vs NodeList: Complete Comparison

Now that you understand both, here's how they stack up:

Feature HTMLCollection NodeList
Contains HTML elements only Any node type (elements, text, comments)
Live or Static? Always live Usually static (except childNodes)
Returned by getElementsByClassName
getElementsByTagName
getElementsByName
element.children
querySelectorAll (static)
childNodes (live)
Has .forEach() No Yes
Other array methods No No (only forEach)
Indexed access collection[0] list[0]
.length property Yes Yes
.item() method Yes Yes
Performance Faster (direct reference) Slightly slower (query evaluation)
Use case When you need live updates When you need a stable snapshot

Converting to Real Arrays

Both HTMLCollection and NodeList are limited compared to real arrays. Converting them gives you access to the full array API.

Why Convert?

const items = document.querySelectorAll(".item");

// Want to filter? Can't do this:
items.filter(item => item.classList.contains("active")); // ERROR

// Want to map? Can't do this:
items.map(item => item.textContent); // ERROR

How to Convert

Method 1: Spread Operator (modern, clean)

const itemsArray = [...document.querySelectorAll(".item")];

// Now you have full array methods:
itemsArray.filter(item => item.classList.contains("active"));
itemsArray.map(item => item.textContent);

Method 2: Array.from() (explicit, readable)

const boxesArray = Array.from(document.getElementsByClassName("box"));

// Same full array functionality:
boxesArray.forEach(box => console.log(box));
boxesArray.reduce((acc, box) => acc + box.offsetHeight, 0);

Both methods work identically. Choose based on preference.

Benefits of Converting

// Before: limited iteration
const items = document.querySelectorAll(".item");
items.forEach(item => {
  console.log(item);
}); // forEach works but that's it

// After: full array power
const itemsArray = [...items];
const activeItems = itemsArray.filter(item => item.classList.contains("active"));
const heights = activeItems.map(item => item.offsetHeight);
const totalHeight = heights.reduce((sum, h) => sum + h, 0);

Performance Considerations

Understanding performance helps you choose the right approach.

Live Collections and Performance

Live collections re-query the DOM every time you access them:

// Bad: queries DOM on every iteration
for (let i = 0; i < document.getElementsByClassName("item").length; i++) {
  // DOM query runs repeatedly
}

// Better: cache the collection
const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
  // Length is accessed multiple times but collection is cached
}

// Best: cache the length too
const items = document.getElementsByClassName("item");
const len = items.length;
for (let i = 0; i < len; i++) {
  // No repeated lookups
}

When to Use Which

Use querySelectorAll() (static NodeList) when:

  • You're iterating and modifying the DOM

  • You need a stable snapshot

  • You want forEach without converting

  • Safety is more important than live updates

Use getElementsByClassName/Tag (HTMLCollection) when:

  • You specifically need live tracking of DOM changes

  • You're monitoring dynamic content

  • Micro-performance matters (it's slightly faster)

In practice: Most developers default to querySelectorAll() because it's safer and has forEach(). Only use live collections when you explicitly need the live behavior.


Real-World Example: Interactive Cards

Let's build something practical to see these concepts in action.

<!DOCTYPE html>
<html>
<body>
  <div class="card">Card 1</div>
  <div class="card">Card 2</div>
  <div class="card">Card 3</div>
  <button id="addCard">Add Card</button>

  <script src="script.js"></script>
</body>
</html>
// Using querySelectorAll (static NodeList)
function setupCards() {
  const cards = document.querySelectorAll(".card");

  // forEach works on NodeList
  cards.forEach((card, index) => {
    card.addEventListener("click", () => {
      console.log(`Clicked card ${index + 1}`);
      card.style.background = "#e0e0e0";
    });
  });

  console.log(`Set up ${cards.length} cards`);
}

// Add new card dynamically
document.getElementById("addCard").addEventListener("click", () => {
  const newCard = document.createElement("div");
  newCard.className = "card";
  newCard.textContent = `Card ${document.querySelectorAll(".card").length + 1}`;
  document.body.insertBefore(newCard, document.getElementById("addCard"));

  // Need to call setupCards again to add listeners to new card
  setupCards();
});

setupCards();

The problem: Static NodeList doesn't include new cards. You need to re-run setup.

Alternative with live collection:

// Using getElementsByClassName (live HTMLCollection)
function setupCardsLive() {
  const cards = document.getElementsByClassName("card");

  // Convert to array to use forEach
  [...cards].forEach((card, index) => {
    card.addEventListener("click", () => {
      console.log(`Clicked card ${index + 1}`);
    });
  });

  // cards.length updates automatically
  console.log(`Currently ${cards.length} cards`);
}

// The collection stays up to date
setInterval(() => {
  const cards = document.getElementsByClassName("card");
  console.log(`Cards in DOM: ${cards.length}`);
}, 2000);

Better approach: Use event delegation (beyond this article's scope) to avoid the problem entirely.



Common Mistakes to Avoid

1. Assuming HTMLCollection has forEach

// ERROR
document.getElementsByClassName("item").forEach(item => {
  console.log(item);
});

// Fix: convert to array first
[...document.getElementsByClassName("item")].forEach(item => {
  console.log(item);
});

2. Not accounting for live collection updates

// Bug: only removes every other element
const items = document.getElementsByClassName("item");
for (let i = 0; i < items.length; i++) {
  items[i].remove(); // collection shrinks during loop
}

// Fix: use static NodeList or convert to array
const items = [...document.getElementsByClassName("item")];
items.forEach(item => item.remove());

3. Thinking querySelectorAll is live

const items = document.querySelectorAll(".item");
console.log(items.length); // 3

// Add more .item elements
document.body.innerHTML += '<div class="item">New</div>';

console.log(items.length); // Still 3, not 4

4. Mixing up which methods return which collection

// Returns HTMLCollection (live)
getElementsByClassName()
getElementsByTagName()
element.children

// Returns NodeList (static)
querySelectorAll()

// Returns NodeList (live)
element.childNodes

Points to Note

  1. HTMLCollection contains only element nodes and is always live

  2. NodeList can contain any node type and is usually static (except childNodes)

  3. Live collections update automatically when the DOM changes this can break loops

  4. Static collections are snapshots safer for iteration

  5. Neither is a real array, but NodeList has forEach()

  6. Convert to arrays for full array methods: [...collection] or Array.from(collection)

  7. Default to querySelectorAll() unless you specifically need live behavior

  8. Cache collections and their lengths for better performance

Understanding these differences will save you from hours of debugging wierd bugs where your loops skip elements or run infinitely.