Alpine.js Project — Build a Complete Real-World Application
In this tutorial, you'll learn to build a complete Alpine.js project. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Build a complete real-world Alpine.js task management application that combines x-data, x-model, x-show, x-for, x-transition, and plugins into a fully functional single-page app.
What You'll Learn
By the end of this tutorial, you'll have built a task manager with add/edit/delete, search filtering, persistence via $persist, modal dialogs with focus trapping, and smooth animations.
Why It Matters
Reading tutorials teaches syntax. Building a complete project teaches architecture: how components interact, how state flows, and how to organize Alpine code for maintainability and performance.
Real-World Use
DodaZIP's internal project tracker started as a single Alpine Prototype similar to this project. The patterns you'll learn here scale to real production applications used by thousands of users.
Where This Fits in Your Learning Path
flowchart LR
A["Plugins & Magics"] --> B["**Alpine Project**"]
B --> C["Production Alpine Apps"]
style B fill:#f97316,stroke:#c2410c,color:#fff
style A fill:#e5e7eb,stroke:#9ca3af,color:#374151
style C fill:#22c55e,stroke:#16a34a,color:#fff
Project Overview
We'll build a Task Manager with these features:
- Add new tasks with title, priority, and category
- Edit existing tasks in a modal
- Mark tasks as complete
- Filter by status (all/active/completed)
- Search by keyword
- Sort by priority or date
- Persist data across page loads
- Smooth animations for add/remove
Step 1: Set Up the HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine Task Manager</title>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="taskManager()" class="max-w-2xl mx-auto p-6">
<!-- Components will go here -->
</div>
<script>
function taskManager() {
return {
// State and methods will go here
}
}
</script>
</body>
</html>
Step 2: Define the Component State and Methods
function taskManager() {
return {
tasks: Alpine.$persist([]),
newTask: { title: '', priority: 'medium', category: 'general' },
search: '',
filter: 'all',
sortBy: 'date',
editingTask: null,
showModal: false,
nextId: Alpine.$persist(1),
addTask() {
if (!this.newTask.title.trim()) return
this.tasks.push({
id: this.nextId++,
title: this.newTask.title,
priority: this.newTask.priority,
category: this.newTask.category,
done: false,
createdAt: new Date().toISOString()
})
this.newTask = { title: '', priority: 'medium', category: 'general' }
},
editTask(task) {
this.editingTask = { ...task }
this.showModal = true
},
saveEdit() {
const idx = this.tasks.findIndex(t => t.id === this.editingTask.id)
if (idx !== -1) this.tasks[idx] = { ...this.editingTask }
this.showModal = false
this.editingTask = null
},
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id)
},
toggleTask(id) {
const task = this.tasks.find(t => t.id === id)
if (task) task.done = !task.done
},
get filteredTasks() {
let result = [...this.tasks]
if (this.filter === 'active') result = result.filter(t => !t.done)
if (this.filter === 'completed') result = result.filter(t => t.done)
if (this.search) {
const q = this.search.toLowerCase()
result = result.filter(t => t.title.toLowerCase().includes(q))
}
if (this.sortBy === 'priority') {
const order = { high: 0, medium: 1, low: 2 }
result.sort((a, b) => order[a.priority] - order[b.priority])
} else {
result.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
}
return result
},
get stats() {
return {
total: this.tasks.length,
active: this.tasks.filter(t => !t.done).length,
completed: this.tasks.filter(t => t.done).length
}
}
}
}
Expected behavior: The task manager maintains state with persistence. Methods handle CRUD operations. FilteredTasks is a computed getter for display.
Step 3: Build the Add Task Form
<form @submit.prevent="addTask" class="flex gap-2 mb-6">
<input x-model="newTask.title" placeholder="Task title" required class="flex-1 p-2 border rounded">
<select x-model="newTask.priority" class="p-2 border rounded">
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select x-model="newTask.category" class="p-2 border rounded">
<option value="general">General</option>
<option value="work">Work</option>
<option value="personal">Personal</option>
</select>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded">Add</button>
</form>
Expected output: A form with a text input, two selects, and a submit button. Submitting adds a task to the list.
Step 4: Build the Filter and Search Bar
<div class="flex gap-2 mb-4">
<input x-model="search" placeholder="Search tasks..." class="flex-1 p-2 border rounded">
<select x-model="filter" class="p-2 border rounded">
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
<select x-model="sortBy" class="p-2 border rounded">
<option value="date">Date</option>
<option value="priority">Priority</option>
</select>
</div>
Expected output: Controls for searching, filtering by status, and sorting. The task list updates reactively.
Step 5: Build the Task List
<div class="space-y-2">
<template x-for="task in filteredTasks" :key="task.id">
<div x-show="true" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100"
class="flex items-center gap-3 p-3 bg-white rounded shadow-sm border">
<input type="checkbox" :checked="task.done" @change="toggleTask(task.id)" class="w-5 h-5">
<div class="flex-1">
<span x-text="task.title" :class="task.done ? 'line-through text-gray-400' : ''" class="font-medium"></span>
<div class="flex gap-2 text-xs text-gray-500 mt-1">
<span :class="{'text-red-500': task.priority === 'high', 'text-yellow-500': task.priority === 'medium', 'text-green-500': task.priority === 'low'}"
x-text="task.priority"></span>
<span x-text="task.category"></span>
</div>
</div>
<button @click="editTask(task)" class="text-sm text-blue-500 hover:underline">Edit</button>
<button @click="deleteTask(task.id)" class="text-sm text-red-500 hover:underline">Delete</button>
</div>
</template>
<p x-show="filteredTasks.length === 0" class="text-gray-400 text-center py-8">No tasks found</p>
</div>
Expected output: A list of tasks with checkboxes, priority colors, edit and delete buttons. Adding, editing, or deleting tasks animates smoothly.
Step 6: Build the Stats Bar
<div class="mt-6 flex gap-4 text-sm text-gray-500">
<span x-text="`Total: ${stats.total}`"></span>
<span x-text="`Active: ${stats.active}`"></span>
<span x-text="`Completed: ${stats.completed}`"></span>
</div>
Expected output: A stats bar showing task counts that update in real time.
Step 7: Build the Edit Modal
<div x-show="showModal" x-trap.noscroll="showModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
@keydown.escape.window="showModal = false">
<div @click.outside="showModal = false" class="bg-white p-6 rounded-lg w-full max-w-md">
<h2 class="text-lg font-bold mb-4">Edit Task</h2>
<template x-if="editingTask">
<div class="space-y-3">
<input x-model="editingTask.title" class="w-full p-2 border rounded" placeholder="Task title">
<select x-model="editingTask.priority" class="w-full p-2 border rounded">
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select x-model="editingTask.category" class="w-full p-2 border rounded">
<option value="general">General</option>
<option value="work">Work</option>
<option value="personal">Personal</option>
</select>
<div class="flex gap-2 pt-2">
<button @click="saveEdit" class="px-4 py-2 bg-blue-500 text-white rounded">Save</button>
<button @click="showModal = false" class="px-4 py-2 bg-gray-200 rounded">Cancel</button>
</div>
</div>
</template>
</div>
</div>
Expected output: Clicking Edit opens a modal with pre-filled values. Focus is trapped inside. Escape or clicking outside closes it. Save updates the task.
Complete Application
The full application combines all steps above. Paste them together into a single HTML file and open it in a browser. The task manager works with persistence across page refreshes, smooth animations, and full CRUD functionality.
Common Mistakes
1. Not using $persist for the tasks array
Without $persist, all tasks disappear on page refresh. Always persist user data.
2. Forgetting to use :key in x-for
Without unique keys, Alpine may incorrectly reuse DOM nodes when the list changes, causing visual bugs.
3. Not handling the empty state
When no tasks match the filter, show a helpful message instead of a blank page.
4. Editing the task object directly instead of cloning
Always clone the task for editing (using spread operator) so canceling doesn't modify the original.
5. Overcomplicating the data flow
Keep state in a single component for simple apps. Use stores and events only when multiple separate components need to communicate.
Practice Questions
Why use a function for the component data instead of an object? Functions create a fresh instance each time, preventing shared mutable state between multiple components.
What does Alpine.$persist() do? It wraps a value so it's automatically saved to localStorage and restored on page load.
Why clone the task object in editTask? To avoid mutating the original task while editing. If the user cancels, the original is unchanged.
What is the purpose of x-trap.noscroll? It traps keyboard focus inside the modal and prevents background scrolling, improving Accessibility and UX.
How does the filteredTasks getter work? It's a computed property that returns a new array filtered by status, search query, and sorted by the selected criteria.
Challenge
Extend the project with categories management (add/edit/delete categories), due dates, and a calendar view. Use a separate Alpine component for the category manager with cross-component communication via $store.
FAQ
What's Next
Congratulations on building a complete Alpine application! Continue with related topics:
| Tutorial | What You'll Learn |
|---|---|
| Transitions and Plugins | Advanced animation and plugin patterns |
| JavaScript Array Methods | The JavaScript fundamentals behind Alpine's reactivity |
Related topics: localStorage and client-side persistence, CRUD application patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro