HTMLCollection vs NodeList: Understanding JavaScript's DOM Collections
Understand HTMLCollection vs NodeList in detail

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 | getElementsByClassNamegetElementsByTagNamegetElementsByNameelement.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
forEachwithout convertingSafety 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
HTMLCollection contains only element nodes and is always live
NodeList can contain any node type and is usually static (except
childNodes)Live collections update automatically when the DOM changes this can break loops
Static collections are snapshots safer for iteration
Neither is a real array, but NodeList has
forEach()Convert to arrays for full array methods:
[...collection]orArray.from(collection)Default to
querySelectorAll()unless you specifically need live behaviorCache 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.




