Alpine.js Tutorial: Build a Todo App

Learn how to use Alpine.js to create interactive web applications with minimal JavaScript

Beginner JavaScript Frontend

Alpine.js is a rugged, minimal framework for composing JavaScript behavior in your markup. It offers the reactive and declarative nature of big frameworks like Vue or React at a much lower cost.

Why Choose Alpine.js?

  • Lightweight: Only ~7kB minified
  • No build step: Just include the script and start coding
  • Simple syntax: Easy to learn if you know HTML
  • Reactive: Automatically updates the DOM when data changes
Prerequisites: Basic knowledge of HTML and JavaScript will help you follow along better.

Step-by-Step Alpine.js Tutorial

1 Install Alpine.js

There are several ways to include Alpine.js in your project:

CDN (Simplest Method)

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

NPM

npm install alpinejs

Then import it in your JavaScript file:

import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()

2 Basic Alpine.js Structure

Alpine.js uses directives (special HTML attributes) to add functionality. The basic structure looks like this:

<div x-data="{ count: 0 }">
    <button @click="count++">Increment</button>
    <span x-text="count"></span>
</div>

Key directives:

  • x-data: Declares a component and its data
  • x-text: Sets the text content of an element
  • @click: Handles click events

3 Building a Todo App

Let's create a complete todo application with Alpine.js. We'll implement:

  • Adding new todos
  • Marking todos as complete
  • Deleting todos
  • Filtering todos

HTML Structure

Create a new HTML file and include Alpine.js:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Alpine.js Todo App</title>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center mb-4">Alpine.js Todo App</h1>
        <div x-data="todoApp()">
            <!-- Todo form will go here -->
            <!-- Todo list will go here -->
        </div>
    </div>
</body>
</html>

4 Implementing the Todo App Logic

Add the following JavaScript inside a <script> tag or in a separate file:

<script>
function todoApp() {
    return {
        newTodo: '',
        todos: [],
        filter: 'all',
        
        addTodo() {
            if (this.newTodo.trim() === '') return;
            this.todos.push({
                id: Date.now(),
                text: this.newTodo,
                completed: false
            });
            this.newTodo = '';
        },
        
        removeTodo(id) {
            this.todos = this.todos.filter(todo => todo.id !== id);
        },
        
        toggleTodo(id) {
            const todo = this.todos.find(todo => todo.id === id);
            if (todo) {
                todo.completed = !todo.completed;
            }
        },
        
        get filteredTodos() {
            if (this.filter === 'active') {
                return this.todos.filter(todo => !todo.completed);
            } else if (this.filter === 'completed') {
                return this.todos.filter(todo => todo.completed);
            }
            return this.todos;
        },
        
        clearCompleted() {
            this.todos = this.todos.filter(todo => !todo.completed);
        }
    }
}
</script>

5 Creating the Todo UI

Now let's add the HTML markup that uses our Alpine.js component:

<div class="app-container" x-data="todoApp()">
    <h2 class="text-center mb-4">My Todos</h2>
    
    <!-- Add Todo Form -->
    <form @submit.prevent="addTodo" class="mb-4">
        <div class="input-group">
            <input 
                type="text" 
                class="form-control" 
                placeholder="Add a new todo..." 
                x-model="newTodo"
                aria-label="Add a new todo"
            >
            <button class="btn btn-primary" type="submit">Add</button>
        </div>
    </form>
    
    <!-- Todo List -->
    <ul class="list-group mb-4">
        <template x-for="todo in filteredTodos" :key="todo.id">
            <li class="list-group-item todo-item d-flex justify-content-between align-items-center">
                <div class="form-check">
                    <input 
                        type="checkbox" 
                        class="form-check-input" 
                        :id="'todo-' + todo.id"
                        @change="toggleTodo(todo.id)"
                        :checked="todo.completed"
                    >
                    <label 
                        class="form-check-label" 
                        :for="'todo-' + todo.id"
                        :class="{ completed: todo.completed }"
                        x-text="todo.text"
                    ></label>
                </div>
                <button @click="removeTodo(todo.id)" class="btn btn-sm btn-danger">×</button>
            </li>
        </template>
    </ul>
    
    <!-- Filters -->
    <div class="d-flex justify-content-between align-items-center">
        <div x-text="`${todos.filter(t => !t.completed).length} items left`"></div>
        <div class="btn-group">
            <button 
                @click="filter = 'all'" 
                class="btn btn-sm" 
                :class="{ 'btn-primary': filter === 'all' }"
            >
                All
            </button>
            <button 
                @click="filter = 'active'" 
                class="btn btn-sm" 
                :class="{ 'btn-primary': filter === 'active' }"
            >
                Active
            </button>
            <button 
                @click="filter = 'completed'" 
                class="btn btn-sm" 
                :class="{ 'btn-primary': filter === 'completed' }"
            >
                Completed
            </button>
        </div>
        <button @click="clearCompleted" class="btn btn-sm btn-link">Clear completed</button>
    </div>
</div>

6 Live Demo

Here's how your Alpine.js todo app should look and work:

My Todos

Alpine.js Learning Path

To master Alpine.js, follow this study plan:

Week 1: Fundamentals

  • Understand the core directives: x-data, x-show, x-if, x-for
  • Learn event handling with @click, @input, etc.
  • Practice binding data with x-model
  • Build simple components like counters, toggles, and accordions

Week 2: Intermediate Concepts

  • Learn about x-init for initialization
  • Understand $el, $refs, and $event magic properties
  • Explore transitions with x-transition
  • Build more complex components like modals, tabs, and dropdowns

Week 3: Advanced Topics

  • Learn about Alpine.js stores for state management
  • Understand how to use Alpine.js with other libraries
  • Explore custom directives
  • Build a complete SPA with Alpine.js

Week 4: Real-world Projects

  • Build a CRUD application
  • Create a dashboard with multiple interactive components
  • Integrate Alpine.js with a backend API
  • Optimize performance for larger applications

Conclusion

Alpine.js is a fantastic choice when you need to sprinkle interactivity into your web pages without the overhead of larger frameworks. It's particularly useful for:

  • Server-rendered applications that need some JavaScript enhancement
  • Small to medium-sized projects where a full framework would be overkill
  • Teams that want to move quickly without complex build setups
  • Adding interactivity to static sites

The todo app we built demonstrates Alpine.js's core concepts in action. From here, you can explore more advanced features like components, stores, and transitions.

Next Steps: Try enhancing the todo app with features like due dates, categories, or persistence to localStorage.