Skip to content

Alpine.js x-for Directive — Complete Guide with Examples

DodaTech Updated 2026-06-28 6 min read

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

  1. What is the correct syntax for x-for? x-for="item in array" or x-for="(item, index) in array".

  2. Why use :key in x-for loops? Keys help Alpine identify which items changed, moved, or were removed, enabling efficient DOM updates.

  3. Can x-for iterate over objects? Yes. Use x-for="(value, key) in object" to iterate over object properties.

  4. What happens when you add an item to the array? Alpine detects the change and renders a new template instance for the new item.

  5. 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

Can x-for be used with x-model inside the loop?

Yes. Each iteration gets its own scope. Use an array of objects and bind x-model to a property like item.name.

Does x-for re-render the entire list on every change?

No. Alpine uses a virtual DOM diffing algorithm with key-based reconciliation to only update changed items.

Can I use x-for with a computed array?

Yes. You can use any expression that evaluates to an array: filtered arrays, mapped arrays, or the result of a function call.

What is the scope inside an x-for loop?

Each iteration has access to the current item, the index, and the parent component's data through scope inheritance.

How do I limit the number of rendered items?

Use array methods like slice, or use x-show with an index condition to hide items beyond a certain limit.


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