Alpine.js x-for Directive — Complete Guide with Examples
In this tutorial, you'll learn about the Alpine.js x-for directive. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
The Alpine.js x-for directive iterates over arrays and objects to render lists of elements, similar to JavaScript's forEach but with automatic DOM updates when data changes.
What You'll Learn
By the end of this tutorial, you'll render lists with x-for, use the index variable, add unique keys for performance, iterate over object properties, and nest loops.
Why It Matters
Dynamic lists are everywhere in web applications: product listings, comment threads, notification feeds, and search results. Manual DOM manipulation for lists is tedious and error-prone. x-for handles creation, update, and removal of list items automatically.
Real-World Use
DodaZIP's file browser uses x-for to render the list of archive contents. When a user extracts files, the list updates reactively. Each file entry uses x-for with a key based on file path for efficient re-rendering.
Where This Fits in Your Learning Path
flowchart LR
A["x-if Directive"] --> B["**x-for Directive**"]
B --> C["x-ref & x-teleport"]
C --> D["Magics & Store"]
D --> E["Advanced Alpine Patterns"]
style B fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style E fill:#22c55e,stroke:#16a34a,color:#fff
What is x-for?
x-for must be used on a <template> element. It iterates over an array and renders the template's content for each item. The syntax is x-for="item in items" or x-for="(item, index) in items".
Think of x-for like a cookie cutter. You have a sheet of dough (the array) and a single cookie cutter (the template). The cutter stamps out one cookie for each piece of dough.
<div x-data="{ fruits: ['Apple', 'Banana', 'Cherry'] }">
<ul>
<template x-for="(fruit, index) in fruits" :key="index">
<li><span x-text="index + 1"></span>. <span x-text="fruit"></span></li>
</template>
</ul>
</div>
Expected output: A numbered list: "1. Apple", "2. Banana", "3. Cherry".
Adding and Removing Items
x-for reacts to array mutations. Use push, pop, splice, or reassignment to update the list.
<div x-data="{ items: ['Task 1', 'Task 2'], newItem: '' }">
<ul>
<template x-for="(item, index) in items" :key="index">
<li>
<span x-text="item"></span>
<button @click="items.splice(index, 1)" class="text-red-500 ml-2">X</button>
</li>
</template>
</ul>
<div class="mt-2">
<input x-model="newItem" placeholder="New item">
<button @click="items.push(newItem); newItem = ''">Add</button>
</div>
</div>
Expected output: An editable list with add and remove buttons. The list updates reactively.
Using Keys for Performance
Keys help Alpine identify which items changed, moved, or were removed. Without keys, Alpine may recreate all items when the array changes.
<div x-data="{ users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' }
]}">
<template x-for="user in users" :key="user.id">
<div>
<span x-text="user.name"></span>
<button @click="users = users.filter(u => u.id !== user.id)">Remove</button>
</div>
</template>
<button @click="users = [...users, { id: Date.now(), name: 'New User' }]">Add User</button>
</div>
Expected output: Each user has a unique key. Removing and adding items reuses existing DOM nodes when possible, improving performance.
Iterating Over Objects
Use x-for="(value, key) in object" to iterate over object properties.
<div x-data="{ person: { name: 'Alice', age: 30, city: 'New York' } }">
<ul>
<template x-for="(value, key) in person" :key="key">
<li><strong x-text="key"></strong>: <span x-text="value"></span></li>
</template>
</ul>
</div>
Expected output: "name: Alice", "age: 30", "city: New York".
Nested x-for Loops
Nest template elements to render multi-dimensional data.
<div x-data="{ matrix: [[1, 2], [3, 4], [5, 6]] }">
<table border="1">
<template x-for="(row, rowIndex) in matrix" :key="rowIndex">
<tr>
<template x-for="(cell, colIndex) in row" :key="colIndex">
<td x-text="cell" class="p-2"></td>
</template>
</tr>
</template>
</table>
</div>
Expected output: A 3x2 table displaying the matrix values.
Common Mistakes
1. Forgetting the template wrapper
<!-- Wrong: x-for must be on a <template> -->
<li x-for="item in items" x-text="item"></li>
<!-- Correct -->
<template x-for="item in items" :key="item">
<li x-text="item"></li>
</template>
2. Not using :key when items have unique IDs
Without keys, Alpine may recreate all items on every array change, causing performance issues and lost state.
3. Mutating arrays without triggering reactivity
Replacing an item by index (items[0] = newValue) may not trigger an update. Use reassignment or splice.
4. Using x-for on a root element inside another x-for
Each x-for needs its own template. Nest templates for nested loops.
5. Creating duplicate keys
Using the array index as a key is fine for static lists. For dynamic lists with removals, use a unique ID to avoid incorrect DOM reuse.
Practice Questions
What is the correct syntax for x-for?
x-for="item in array"orx-for="(item, index) in array".Why use :key in x-for loops? Keys help Alpine identify which items changed, moved, or were removed, enabling efficient DOM updates.
Can x-for iterate over objects? Yes. Use
x-for="(value, key) in object"to iterate over object properties.What happens when you add an item to the array? Alpine detects the change and renders a new template instance for the new item.
What modifier is used to iterate over a range? Alpine doesn't have a range syntax. Create an array dynamically or use Object.keys.
Challenge
Build a sortable, filterable product list. Use x-for to render products from an array. Include a search input that filters the array, and buttons to sort by name or price. Each product should have a delete button.
FAQ
Mini Project
Build a todo list application with x-for. Features: add todos with a form, toggle completion with a checkbox, filter by all/active/completed, delete individual todos, and show the count of remaining items.
<div x-data="{ todos: [], newTodo: '', filter: 'all' }">
<form @submit.prevent="todos.push({ id: Date.now(), text: newTodo, done: false }); newTodo = ''">
<input x-model="newTodo" required placeholder="Add a todo" class="w-full p-2 border rounded">
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded mt-2">Add</button>
</form>
<div class="mt-4 flex gap-2">
<button @click="filter = 'all'" :class="filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'" class="px-3 py-1 rounded">All</button>
<button @click="filter = 'active'" :class="filter === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-200'" class="px-3 py-1 rounded">Active</button>
<button @click="filter = 'completed'" :class="filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'" class="px-3 py-1 rounded">Completed</button>
</div>
<ul class="mt-4 space-y-2">
<template x-for="todo in todos.filter(t => filter === 'all' ? true : filter === 'active' ? !t.done : t.done)" :key="todo.id">
<li class="flex items-center gap-2 p-2 bg-gray-50 rounded">
<input type="checkbox" x-model="todo.done">
<span x-text="todo.text" :class="todo.done ? 'line-through text-gray-400' : ''"></span>
<button @click="todos = todos.filter(t => t.id !== todo.id)" class="ml-auto text-red-500 text-sm">Delete</button>
</li>
</template>
</ul>
<p class="mt-4 text-sm text-gray-500" x-text="`${todos.filter(t => !t.done).length} items remaining`"></p>
</div>
What's Next
Move on to DOM references and teleporting:
| Tutorial | What You'll Learn |
|---|---|
| x-ref Directive | Reference DOM elements directly in expressions |
| x-teleport Directive | Move elements to different parts of the DOM |
Related topics: JavaScript array methods, Vue.js list rendering comparison.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro