foreach is the primary iteration directive. The element that carries the directive IS the repeating template — it is removed from the DOM, and clones are inserted as siblings between comment markers for each item in the array.
The element with foreach (or each / for) is the template itself. No wrapper element is needed — the loop element repeats as a sibling:
<div get="/posts" as="posts">
<ul>
<li foreach="post in posts" key="post.id">
<h2 bind="post.title"></h2>
<p bind="post.body"></p>
<span bind="'#' + $index"></span>
</li>
</ul>
</div>Each <li> clone is inserted directly inside the <ul>. The original <li foreach="..."> element is removed and used as the template source.
Use the template attribute to reference a <template> element by ID. The loop element is still the repeating unit — clones of the referenced template are inserted as siblings:
<div get="/posts" as="posts">
<ul>
<li foreach="post in posts" key="post.id" template="postCard"></li>
</ul>
</div>
<template id="postCard">
<article>
<h2 bind="post.title"></h2>
<p bind="post.body"></p>
<span bind="'#' + $index"></span>
</article>
</template><ul>
<li foreach="item in menuItems"
index="idx"
key="item.id"
filter="item.active"
sort="item.order"
limit="10"
offset="0"
else="noItemsTpl">
<a bind-href="item.link">
<span bind="idx + 1"></span> - <span bind="item.label"></span>
</a>
</li>
</ul>
<template id="noItemsTpl">
<li>No items available</li>
</template>Place else="templateId" on the loop element to reference a <template> for the empty state. The template renders when the list is empty ([]) or null/undefined/non-array — e.g. API state before the first fetch resolves:
<article foreach="item in items" else="noItems">
<h2 bind="item.title"></h2>
</article>
<template id="noItems">
<p class="empty-state">No items found.</p>
</template>When items is an empty array ([]), null, undefined, or any non-array value, the template content replaces the loop output. When items are present, the template is removed and items render normally. Both bare ID (else="noItems") and hash syntax (else="#noItems") are accepted.
Breaking change (v1.15): The sibling
elsepattern (<li else>No items</li>placed after a loop element) has been removed. Useelse="templateId"on the loop element itself instead.
| Attribute | Description |
|---|---|
foreach |
"item in array" — variable name and source expression |
else |
Template ID rendered when the array is empty or null/undefined/non-array (e.g. else="noItemsTpl") |
template |
ID of the <template> element to clone for each item (optional — when omitted, the element's own children are the template) |
index |
Variable name for the index (default: $index) |
key |
Unique key expression for DOM diffing |
filter |
Expression to filter items (like Array.filter) |
sort |
Property path to sort by (prefix with - for descending) |
limit |
Maximum number of items to render |
offset |
Number of items to skip |
animate / animate-enter |
CSS class added to each new item on insert |
animate-leave |
CSS class added to items before removal |
animate-stagger |
Delay (ms) between each item's enter animation |
animate-duration |
Max duration (ms) before leave animation is force-completed |
Empty-state rendering uses the else attribute on the loop element to reference a <template> by ID.
each and for are aliases for foreach. They share the same handler and support all the same attributes — filter, sort, limit, offset, key, animate-*, else, template, index, and loop variables.
<!-- All three are equivalent — the element repeats as siblings -->
<li foreach="item in items" key="item.id">...</li>
<li each="item in items" key="item.id">...</li>
<li for="item in items" key="item.id">...</li>Use whichever reads best for your context. foreach is the canonical name used throughout this documentation.
The legacy from syntax is still supported but deprecated. Using it emits a console warning.
<!-- DEPRECATED — still works, but warns -->
<div foreach="item" from="items" template="tpl"></div>
<!-- Use this instead -->
<div foreach="item in items" template="tpl"></div>The from attribute will be removed in a future major version. Migrate to the "item in array" syntax.
By default, when the source array changes, the loop directive performs a full rebuild — all child nodes are disposed and recreated from scratch. This is simple and correct, but destroys and re-mounts DOM nodes on every update.
When you supply a key attribute, the directive switches to key-based reconciliation: existing DOM nodes for items whose key is still in the list are reused and their context is updated in place. Only items that genuinely appeared or disappeared trigger DOM mutations.
<!-- Without key: full rebuild on every change -->
<li foreach="item in items" template="itemTpl"></li>
<!-- With key: only changed items are added/removed -->
<li foreach="item in items" key="item.id" template="itemTpl"></li>The key value must be unique and stable across renders — typically a database ID or UUID. Using a non-unique key (e.g. $index) defeats reconciliation since items will always appear to match.
| Use case | Recommendation |
|---|---|
| Static lists, rendered once | No key needed |
| Lists with < ~10 items, infrequent updates | No key needed (full rebuild is negligible) |
| Large lists (50+ items) with frequent updates | Use key |
push / splice / sort on reactive arrays |
Use key to preserve focus, scroll, and input state |
| Items with embedded inputs, video, canvas | Use key — full rebuild resets state |
After a reorder (e.g. sort), the reconciler calls $notify() on the context of each retained clone. This propagates the updated $index, $first, $last, $even, $odd, and $count values to all child watchers, including nested bindings and class expressions that depend on position.
<template id="itemTpl">
<!-- $index, $odd, $first re-render correctly after sort or reorder -->
<div class-striped="$odd" class-first="$first">
<span bind="($index + 1) + '. ' + item.name"></span>
</div>
</template>Inside any loop, these variables are automatically available:
| Variable | Description |
|---|---|
$index |
Current index (0-based) |
$count |
Total number of items |
$first |
true if first item |
$last |
true if last item |
$even |
true if index is even |
$odd |
true if index is odd |
<ul>
<li foreach="item in items">
<span class-first="$first"
class-last="$last"
class-striped="$odd">
<span bind="($index + 1) + ' of ' + $count"></span>
<span bind="item.name"></span>
</span>
</li>
</ul>Child loops can access parent scope variables. Each loop element repeats as siblings inside its container:
<div foreach="category in categories">
<h3 bind="category.name"></h3>
<p foreach="product in category.products">
<!-- Access both product AND category from parent scope -->
<span bind="category.name"></span>: <span bind="product.name"></span>
</p>
</div>With external templates:
<section foreach="category in categories" template="catTpl"></section>
<template id="catTpl">
<h3 bind="category.name"></h3>
<div foreach="product in category.products" template="prodTpl"></div>
</template>
<template id="prodTpl">
<p><span bind="category.name"></span>: <span bind="product.name"></span></p>
</template>Loop directives are fully reactive. When the source array changes (push, splice, sort, or full reassignment), the loop re-renders automatically. No $notify() call is needed when mutations happen inside HTML expressions:
<div state="{ items: ['A', 'B', 'C'] }">
<ul>
<li foreach="item in items" bind="item"></li>
</ul>
<button on:click="items.push('New')">Add Item</button>
<!-- Loop updates automatically -->
</div>Note:
foreach/each/foriterate over arrays only. Object iteration is not directly supported — use thekeysorvaluesfilter to convert objects to arrays first:<span foreach="key in settings | keys"> <span bind="key"></span>: <span bind="settings[key]"></span> </span>
The most common loop mistake is placing foreach/each/for on a container element instead of the repeating child. The loop element IS the repeating unit — it is removed from the DOM and cloned as siblings between comment markers. Putting the directive on a <ul> or <tbody> produces N copies of the entire container, which is invalid HTML and almost never intended.
<!-- WRONG — each on <ul> creates N <ul> elements, each with one <li> -->
<ul each="item in items">
<li bind="item"></li>
</ul>
<!-- RIGHT — each on <li> creates N <li> elements inside one <ul> -->
<ul>
<li each="item in items" bind="item"></li>
</ul>The same applies to tables — the directive belongs on <tr>, not on <table> or <tbody>:
<!-- WRONG — produces N <tbody> elements -->
<table>
<tbody each="row in rows">
<tr><td bind="row.name"></td></tr>
</tbody>
</table>
<!-- RIGHT — produces N <tr> elements inside one <tbody> -->
<table>
<tbody>
<tr each="row in rows"><td bind="row.name"></td></tr>
</tbody>
</table>Rule of thumb: Always ask "which element should repeat?" and put the loop directive on that element. The parent element becomes the container automatically.
- Conditionals —
else="templateId"on the loop element renders a template for empty lists - Templates — external templates referenced by loops
- Animations —
animate-staggerfor list enter/leave effects - Filters & Pipes —
count,first,last,reverse,sortByfilters - Drag & Drop —
drag-listfor sortable lists
Previous: Conditionals ← | Next: Templates →