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