Live and Static DOM Collections in JavaScript

In Brief
getElementsBy* methods commonly return live HTMLCollection views which change with the DOM, whilst querySelectorAll() returns a static NodeList snapshot of matches at query time. The producing API determines liveness, and neither interface is an Array. Where stable membership or ordering is important, ensure that you snapshot a live collection before mutation.
A loop removes every element with the class expired, but half of them remain in the page. Run the code again and the rest disappear. The selector was correct and the loop reached its final condition, so the result looks impossible at first.
The collection changed underneath the loop. Removing its first element moved the next match into index zero, then the loop continued at index one and skipped it.
Whether that can happen depends on the API which produced the DOM collection.
A Collection is Not an Array
An HTMLCollection or NodeList can look like an Array because it has a numeric length and indexed items:
var tasks = document.getElementsByClassName('task');console.log(tasks.length);console.log(tasks[0]);That shape is why these values are often called array‑like objects. It does not give them Array.prototype methods. In a 2016 browser, this is not safe:
tasks.map(function (task) { return task.textContent;});tasks.map is not a function. The collection has its own interface, not an Array with an unusual name. Liveness is a separate characteristic again: it describes whether later DOM mutations are reflected by that particular collection.
Both interfaces provide item(index) as well as indexed access, so tasks.item(0) and tasks[0] identify the same first element when one exists. That resemblance is useful for ordinary loops, but Array.isArray(tasks) is still false. Converting the value is a real change of interface and, for a live source, a change from a current view to fixed membership.
Live Collections are Views of the Current DOM
Use one list for both examples:
<ul id="task-list"> <li class="task">Email the client</li> <li class="task">Review the brief</li> <li class="task">Send the invoice</li></ul>getElementsByClassName() returns an HTMLCollection. The collection is live, so it represents the matching elements in the current DOM rather than remembering only those present when the method ran:
var list = document.getElementById('task-list');var liveTasks = list.getElementsByClassName('task');console.log(liveTasks.length); // 3var task = document.createElement('li');task.className = 'task';task.textContent = 'Archive the project';list.appendChild(task);console.log(liveTasks.length); // 4console.log(liveTasks[3].textContent); // Archive the projectThe variable was not assigned again. Reading length or an indexed item after the append uses the collection's current underlying data. The 2015 W3C DOM4 Recommendation describes a live collection as operating on that underlying data rather than a snapshot.
The filter is live as well as the tree structure. If an existing element gains the task class, it enters liveTasks; if its class is removed, it leaves. A mutation made by another function has the same effect as one made beside the loop. Storing the collection in a variable does not freeze the matches or isolate them from other code.
That behaviour can be useful. A piece of code which genuinely needs the current set of form controls can keep the view and read it when required. It is dangerous only when code assumes stable membership whilst changing the structure which determines that membership.
Static Collections are Snapshots
Run querySelectorAll() before appending the same item:
var list = document.getElementById('task-list');var staticTasks = list.querySelectorAll('.task');console.log(staticTasks.length); // 3var task = document.createElement('li');task.className = 'task';task.textContent = 'Archive the project';list.appendChild(task);console.log(staticTasks.length); // 3console.log(list.querySelectorAll('.task').length); // 4The first NodeList keeps the three matches found at query time. A new call makes a new snapshot and sees four. The Selectors API contract for `querySelectorAll()` states directly that its result must be static and that later structural changes are not reflected.
Static does not mean that the elements were copied. The list still contains references to the matching element nodes. If one of those nodes is removed from the document, it remains an item in the old snapshot; changes made to that node can still be observed through the reference.
The snapshot fixes membership, not every property of each member. If staticTasks[0].textContent is changed, reading that property through the snapshot returns the new text. If the element loses its task class, it still occupies its old position in staticTasks because it matched when the query ran. A new query applies the selector to the current DOM and may produce different membership.
Do not turn this into the false rule that NodeList means static. childNodes, for example, returns a live NodeList. The interface name tells you which operations the collection exposes. The producing API tells you whether that instance is live or static.
| Source | Interface | Liveness |
|---|---|---|
getElementsByClassName() | HTMLCollection | live |
querySelectorAll() | NodeList | static |
childNodes | NodeList | live |
Mutation During Iteration
Here is the failure from the opening with four consecutive matches:
<ul id="messages"> <li class="expired">A</li> <li class="expired">B</li> <li class="expired">C</li> <li class="expired">D</li></ul>var messages = document.getElementById('messages');var expired = messages.getElementsByClassName('expired');for (var index = 0; index < expired.length; index += 1) { messages.removeChild(expired[index]);}The first iteration removes A. The live collection immediately becomes [B, C, D]. The loop increments index to one, which now points to C, so it removes C. The collection becomes [B, D], index becomes two, and the condition fails because length is also two. B and D remain.
Removal is not the only way to trigger this. Changing expired[index].className so that the node no longer matches also removes it from the live collection and shifts the following match into its index. The hazard is mutation of the filter or rooted subtree whilst a forward index assumes stable membership.
When stable membership and document order matter, make an Array snapshot before mutating:
var messages = document.getElementById('messages');var expired = Array.from( messages.getElementsByClassName('expired'));for (var index = 0; index < expired.length; index += 1) { messages.removeChild(expired[index]);}Array.from() reads the live collection into a new Array. Removing A no longer changes the Array's length or moves B to another index, so the loop removes A, B, C, and D in order.
There is another deliberate option when the live behaviour is useful:
var expired = messages.getElementsByClassName('expired');while (expired.length > 0) { messages.removeChild(expired[expired.length - 1]);}Consuming from the stable end removes every match, although the removal order is D, C, B, A. Use that only when reverse order has no observable consequence. Snapshotting makes the intent clearer when callbacks, logging, or other side effects depend on document order.
What Worked in 2016
Indexed loops have the lowest compatibility assumption for these collections. Do not base an August 2016 example on NodeList.prototype.forEach(); cross‑browser availability arrived later, and the current MDN record dates broad availability to October 2017. Likewise, for...of support for DOM collections was not a safe universal assumption.
Array.from() belongs to ECMAScript 2015 and is period‑appropriate for an ES2015 codebase. The ECMAScript 2015 definition accepts an iterable or array‑like source. In August 2016 it worked in contemporary Chrome, Firefox, Safari, and Edge releases, but not Internet Explorer. Transpiling syntax does not automatically add this built‑in, so include a tested polyfill or use a supported conversion when the browser policy requires one.
Choose the View or the Snapshot Deliberately
Start with the source. A live collection is useful when code needs a view of matching nodes as the document changes. A static collection or Array snapshot is safer when a loop will add, remove, or reclassify those nodes.
Do not infer liveness from NodeList, and do not infer Array behaviour from indexes and length. Identify the producing API, decide whether membership should change during the operation, and take a snapshot when the answer is no.
Postscript
Aug 2026: This article forms part of an archive restored from a previous version of my website. Its original publication date is accurate. During the restoration, I reviewed and updated it where appropriate for formatting, imagery, broken links, code correctness, and current internal references, whilst preserving the original technical context and intent.
Modern browsers provide more convenient ways to work with many DOM collections than were available when this article was originally published. The distinction between live and static collections, however, remains unchanged and continues to be an important concept when working with the DOM.