Kritim Yantra
Apr 04, 2025
Introduction to Alpine.js
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 (only ~7kB gzipped).
You can include Alpine.js in your project in several ways:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
npm install alpinejs
Then in your JavaScript:
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()
import Alpine from 'alpinejs'
import focus from '@alpinejs/focus' // Optional plugins
Alpine.plugin(focus)
window.Alpine = Alpine
Alpine.start()
The foundation of Alpine is x-data
, which declares a new component scope.
<div x-data="{ count: 0 }">
<button @click="count++">Increment</button>
<span x-text="count"></span>
</div>
Conditionally show elements:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>
Content that shows/hides
</div>
<template x-if="open">
<div>Content that is removed from DOM</div>
</template>
</div>
Dynamically set HTML attributes:
<div x-data="{ isActive: true }">
<span :class="{ 'text-red-500': isActive }">Colored Text</span>
<button :disabled="isActive">Button</button>
</div>
Listen for browser events:
<div x-data="{ count: 0 }">
<button @click="count++">+</button>
<button @click="count--">-</button>
<span x-text="count"></span>
<input @keyup.enter="alert('Enter pressed!')">
</div>
Two-way data binding for form inputs:
<div x-data="{ message: '' }">
<input x-model="message" placeholder="Type something">
<p>You typed: <span x-text="message"></span></p>
</div>
Update text or HTML content:
<div x-data="{ title: 'Hello', content: '<strong>Alpine.js</strong>' }">
<h1 x-text="title"></h1>
<div x-html="content"></div>
</div>
Render lists:
<div x-data="{ items: ['Apple', 'Banana', 'Orange'] }">
<template x-for="item in items" :key="item">
<li x-text="item"></li>
</template>
</div>
Run code when a component initializes:
<div x-data="{ count: 0 }" x-init="count = 5">
<span x-text="count"></span> <!-- Outputs 5 -->
</div>
<div x-data="{ count: 0 }">
<button @click="count--">Decrement</button>
<span x-text="count"></span>
<button @click="count++">Increment</button>
</div>
<div x-data="{
todos: [],
newTodo: '',
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);
todo.completed = !todo.completed;
}
}">
<input
x-model="newTodo"
@keyup.enter="addTodo()"
placeholder="Add a todo"
>
<button @click="addTodo()">Add</button>
<template x-for="todo in todos" :key="todo.id">
<div>
<input
type="checkbox"
@change="toggleTodo(todo.id)"
:checked="todo.completed"
>
<span
x-text="todo.text"
:class="{ 'line-through': todo.completed }"
></span>
<button @click="removeTodo(todo.id)">×</button>
</div>
</template>
</div>
<div x-data="{ open: false }">
<button @click="open = true">Open Modal</button>
<div
x-show="open"
@click.away="open = false"
x-transition
style="display: none"
>
<div>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
<button @click="open = false">Close</button>
</div>
</div>
</div>
<div x-data="{ activeTab: 'tab1' }">
<div>
<button @click="activeTab = 'tab1'">Tab 1</button>
<button @click="activeTab = 'tab2'">Tab 2</button>
<button @click="activeTab = 'tab3'">Tab 3</button>
</div>
<div>
<div x-show="activeTab === 'tab1'">Tab 1 Content</div>
<div x-show="activeTab === 'tab2'">Tab 2 Content</div>
<div x-show="activeTab === 'tab3'">Tab 3 Content</div>
</div>
</div>
<div x-data="{
posts: [],
loading: false,
error: null,
async fetchPosts() {
this.loading = true;
this.error = null;
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
this.posts = await response.json();
} catch (err) {
this.error = 'Failed to fetch posts';
} finally {
this.loading = false;
}
}
}" x-init="fetchPosts()">
<template x-if="loading">
<p>Loading...</p>
</template>
<template x-if="error">
<p x-text="error" class="text-red-500"></p>
</template>
<template x-for="post in posts" :key="post.id">
<div>
<h3 x-text="post.title"></h3>
<p x-text="post.body"></p>
</div>
</template>
</div>
You can create reusable components by extracting the JavaScript object:
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('counter', () => ({
count: 0,
increment() { this.count++ },
decrement() { this.count-- }
}))
})
</script>
<div x-data="counter">
<button @click="decrement()">-</button>
<span x-text="count"></span>
<button @click="increment()">+</button>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
add(item) {
this.items.push(item)
},
remove(id) {
this.items = this.items.filter(item => item.id !== id)
},
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
})
})
</script>
<!-- Component 1 -->
<div x-data>
<button @click="$store.cart.add({ id: 1, name: 'Product', price: 10 })">
Add to Cart
</button>
</div>
<!-- Component 2 -->
<div x-data>
<div x-text="$store.cart.items.length"></div>
<div x-text="'$' + $store.cart.total"></div>
</div>
You can extend Alpine with custom directives:
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('uppercase', (el) => {
el.textContent = el.textContent.toUpperCase()
})
})
</script>
<div x-data>
<p x-uppercase>This text will be uppercase</p>
</div>
Alpine provides several magic properties:
$el
- The current element$refs
- Access to elements marked with x-ref
$watch
- Watch for changes in data$dispatch
- Dispatch custom eventsExample using $watch
:
<div x-data="{ count: 0 }" x-init="$watch('count', value => console.log(value))">
<button @click="count++">Increment</button>
</div>
Alpine has a built-in transition system:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>
Default transition
</div>
<div x-show="open" x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform scale-90"
x-transition:enter-end="opacity-100 transform scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-90">
Custom transition
</div>
</div>
Alpine can be extended with official and community plugins:
npm install @alpinejs/focus
import focus from '@alpinejs/focus'
Alpine.plugin(focus)
Now you can use x-focus
:
<div x-data="{ open: false }">
<button @click="open = true">Open</button>
<div x-show="open">
<input type="text" x-focus>
</div>
</div>
For input masking:
npm install @alpinejs/mask
import mask from '@alpinejs/mask'
Alpine.plugin(mask)
Usage:
<input x-data x-mask="(999) 999-9999" placeholder="(123) 456-7890">
For collapsible content:
npm install @alpinejs/collapse
import collapse from '@alpinejs/collapse'
Alpine.plugin(collapse)
Usage:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-collapse>
Long content that will be collapsed/expanded
</div>
</div>
x-init
wisely: Avoid heavy operations in initialization@input
)x-cloak
: To hide elements until Alpine is initialized<style>
[x-cloak] { display: none !important; }
</style>
<div x-data x-cloak>
Content hidden until Alpine loads
</div>
Feature | Alpine.js | Vue | React |
---|---|---|---|
Size | ~7kB | ~30kB | ~40kB |
Virtual DOM | No | Yes | Yes |
Build Step | Optional | Required | Required |
Learning Curve | Easy | Medium | Medium |
Best For | Enhancements | SPAs | SPAs |
Reactivity | Automatic | Automatic | Manual (useState) |
x-data
: All Alpine code must be inside an x-data
scopex-init
if neededWhile Alpine doesn't have an official testing framework, you can test components with:
Example Jest test:
test('counter increments', () => {
document.body.innerHTML = `
<div x-data="{ count: 0 }">
<button @click="count++"></button>
<span x-text="count"></span>
</div>
`;
Alpine.start();
const button = document.querySelector('button');
const span = document.querySelector('span');
button.click();
expect(span.textContent).toBe('1');
});
<div x-data="{
products: [
{ id: 1, name: 'Laptop', category: 'electronics', price: 999 },
{ id: 2, name: 'Smartphone', category: 'electronics', price: 699 },
{ id: 3, name: 'T-shirt', category: 'clothing', price: 19 },
{ id: 4, name: 'Jeans', category: 'clothing', price: 49 }
],
filters: {
category: '',
maxPrice: 1000
},
get filteredProducts() {
return this.products.filter(product => {
return (
(this.filters.category === '' ||
product.category === this.filters.category) &&
product.price <= this.filters.maxPrice
)
})
}
}">
<div>
<select x-model="filters.category">
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<input
type="range"
x-model="filters.maxPrice"
min="0"
max="1000"
>
<span x-text="'Max Price: $' + filters.maxPrice"></span>
</div>
<div>
<template x-for="product in filteredProducts" :key="product.id">
<div>
<h3 x-text="product.name"></h3>
<p x-text="'Category: ' + product.category"></p>
<p x-text="'Price: $' + product.price"></p>
</div>
</template>
</div>
</div>
Alpine.js is a powerful yet simple framework that brings reactivity to your markup without the overhead of larger frameworks. It's perfect for:
With its declarative syntax, small footprint, and easy learning curve, Alpine.js is an excellent choice for developers who want reactivity without complexity.
Happy coding with Alpine.js!
No comments yet. Be the first to comment!
Please log in to post a comment:
Continue with Google