Alpine.js là thư viện JavaScript nhẹ (15KB minified) giúp bạn viết tương tác UI trực tiếp trong HTML — không cần build step, không cần framework nặng như React/Vue. Chỉ thêm CDN và dùng ngay.
Cài đặt Alpine.js
<!-- CDN -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
npm install alpinejs
1. Cơ bản: Counter, Toggle, Binding
<div x-data="{ count: 0, open: false, name: 'Alpine' }">
<!-- Counter -->
<p>Count: <span x-text="count"></span></p>
<button @click="count++">+</button>
<button @click="count--">-</button>
<button @click="count = 0">Reset</button>
<!-- Toggle -->
<button @click="open = !open">Toggle</button>
<p x-show="open" x-transition>Hello!</p>
<!-- Two-way binding -->
<input type="text" x-model="name">
<p>Hello, <span x-text="name"></span></p>
</div>
2. Danh sách và vòng lặp (x-for)
<ul x-data="{ items: ['Apple', 'Banana', 'Cherry'] }">
<template x-for="(item, index) in items" :key="index">
<li>
<span x-text="item"></span>
<button @click="items.splice(index, 1)">Xoá</button>
</li>
</template>
</ul>
3. Modal
<div x-data="{ open: false }">
<button @click="open = true" class="btn">Mở Modal</button>
<!-- Overlay -->
<div x-show="open" x-transition.opacity
class="fixed inset-0 bg-black/50"
@click="open = false"></div>
<!-- Modal content -->
<div x-show="open" x-transition
@keydown.escape.window="open = false"
class="fixed inset-0 flex items-center justify-center">
<div class="bg-white p-6 rounded-lg max-w-md" @click.outside="open = false">
<h3 class="text-lg font-bold">Modal Title</h3>
<p>Nội dung modal ở đây. Nhấn ESC hoặc click bên ngoài để đóng.</p>
<button @click="open = false" class="btn mt-4">Đóng</button>
</div>
</div>
</div>
4. FAQ Accordion
<div x-data="{ active: null }">
<template x-for="(faq, index) in [
{ q: 'Alpine.js là gì?', a: 'Framework JS nhẹ, viết trực tiếp trong HTML.' },
{ q: 'Có cần build step không?', a: 'Không. Chỉ thêm CDN và dùng.' },
{ q: 'Có dùng được với REST API không?', a: 'Có, dùng fetch() trong x-init.' },
]" :key="index">
<div class="border-b">
<button @click="active = active === index ? null : index"
class="w-full text-left p-3 font-medium flex justify-between">
<span x-text="faq.q"></span>
<span x-text="active === index ? '−' : '+'"></span>
</button>
<div x-show="active === index" x-transition>
<p class="p-3 pt-0 text-gray-600" x-text="faq.a"></p>
</div>
</div>
</template>
</div>
5. Load dữ liệu từ REST API
<div x-data="{
posts: [],
loading: true,
error: null,
async init() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
if (!res.ok) throw new Error('HTTP ' + res.status)
this.posts = await res.json()
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
}
}">
<div x-show="loading">Đang tải...</div>
<div x-show="error" x-text="error" class="text-red-500"></div>
<template x-for="post in posts" :key="post.id">
<div class="p-4 border-b">
<h3 x-text="post.title" class="font-bold"></h3>
<p x-text="post.body" class="text-gray-600"></p>
</div>
</template>
</div>
6. Load thêm (Load More) từ API
<div x-data="{
page: 1,
items: [],
hasMore: true,
loading: false,
async loadMore() {
this.loading = true
const res = await fetch('https://api.example.com/items?page=' + this.page)
const data = await res.json()
this.items.push(...data.items)
this.hasMore = data.hasMore
this.page++
this.loading = false
},
async init() {
await this.loadMore()
}
}">
<template x-for="item in items" :key="item.id">
<div class="p-3 border">
<h4 x-text="item.title"></h4>
<p x-text="item.description"></p>
</div>
</template>
<button x-show="hasMore" @click="loadMore"
:disabled="loading"
class="btn w-full mt-4">
<span x-text="loading ? 'Đang tải...' : 'Xem thêm'"></span>
</button>
<p x-show="!hasMore" class="text-center text-gray-500 mt-4">
Đã hiển thị tất cả
</p>
</div>
7. Dropdown Menu
<div x-data="{ open: false }" class="relative">
<button @click="open = !open"
@keydown.escape.window="open = false"
class="btn">
Menu ▼
</button>
<div x-show="open" @click.outside="open = false"
x-transition
class="absolute right-0 mt-2 w-48 bg-white border rounded-lg shadow-lg">
<a href="#" class="block px-4 py-2 hover:bg-gray-100">Profile</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-100">Settings</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-100">Logout</a>
</div>
</div>
8. Store (global state) và Component
<!-- Định nghĩa global store -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('cart', {
items: [],
get total() { return this.items.reduce((s, i) => s + i.price * i.qty, 0) },
get count() { return this.items.reduce((s, i) => s + i.qty, 0) },
add(product) {
const existing = this.items.find(i => i.id === product.id)
if (existing) { existing.qty++ }
else { this.items.push({ ...product, qty: 1 }) }
},
remove(id) { this.items = this.items.filter(i => i.id !== id) }
})
})
</script>
<!-- Dùng trong bất kỳ component nào -->
<button @click="$store.cart.add({ id: 1, name: 'Product A', price: 100 })">
Thêm vào giỏ
</button>
<span x-text="$store.cart.count"></span> sản phẩm —
<span x-text="$store.cart.total"></span>₫
Best Practices
- x-data gọn: Khai báo biến và method trong object, không viết logic dài trong template — gọi
@click="handler()"thay vì@click="count = count + 1; if(count > 10)..." - x-ref cho DOM: Dùng
x-ref="myEl"+$refs.myElthay vì document.querySelector - $watch: Theo dõi biến:
$watch('count', value => console.log(value)) - Debounce input:
@input.debounce="search($event.target.value)"cho live search - Không lạm dụng: Alpine phù hợp cho interactive UI component, không phải framework cho SPA. Với app phức tạp, dùng React/Vue
- Transition: Luôn dùng
x-transitioncho show/hide — mượt và dễ đọc
Kết luận
Alpine.js là lựa chọn tuyệt vời cho các dự án WordPress theme, landing page, hoặc bất kỳ trang nào cần interactive UI mà không muốn setup Webpack hay React. Chỉ 15KB, chỉ thêm CDN, và viết trực tiếp trong HTML. Phù hợp cho: modal, accordion, tabs, dropdown, load API, counter, cart, form validation.
Tham khảo: alpinejs.dev | GitHub
Bài cùng chủ đề: Swiper Slider | Sourcetree | Dev Tools