JavaScriptを使えば、簡単にインタラクティブなWebアプリケーションが開発できます。しかし、JavaScriptに不慣れな方にとっては、ちょっとした操作のためにプログラミングを書くのは敷居が高く感じるでしょう。
そこで使ってみたいのがAlpine.jsです。以前紹介したhtmxが通信処理に特化しているのに対して、Alpine.jsはUIの操作に特化しています。
今回は、このAlpine.jsを使って、簡単にインタラクティブなUIを作る方法を紹介します。
基本形
Alpine.jsで紹介されている、最も基本的な形は以下の通りです。
<html>
<head>
<title>Alpine.js CDN</title>
<!-- utf-8 -->
<meta charset="utf-8">
<!-- CDNリンクは小文字の "alpinejs" を使用することに注意 -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="{ count: 0 }">
<button x-on:click="count++">Increment</button>
<span x-text="count"></span>
</div>
</body>
</html>

x-*
で記述されている要素がAlpine.jsのディレクティブです。x-data
はデータを定義するディレクティブで、x-on
はイベントを定義するディレクティブです。なお、x-on
は @ で省略できます。
<button @click="count++">Increment</button>
そして、x-text
は要素のテキストをバインドするディレクティブです。これにより、count
の値が変更されると、その値が自動的に表示されます。
たとえば、以下のようなDOMを書けば、カウントダウンしたり、リセット処理を追加できます。
<button @click="count--">Decrement</button>
<button @click="count=0">Reset</button>
他のディレクティブ
Alpine.jsには、他にも多くのディレクティブが用意されています。たとえば、以下のようなディレクティブがあります。
x-show
x-show
ディレクティブは、条件に応じて要素を表示・非表示するディレクティブです。以下はボタンのクリックで表示・非表示を切り替えられます。ボタンを押すと open
の値が反転し、open = true
の場合は表示され、open = false
の場合は非表示になります。
<html>
<head>
<title>Alpine.js CDN</title>
<!-- utf-8 -->
<meta charset="utf-8">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="{ open: false }">
<button @click="open = ! open">トグル</button>
<div x-show="open" @click.outside="open = false">内容...</div>
</div>
</body>
</html>

@click.outside は、要素外をクリックしたときに処理 open = false
が実行されるディレクティブです。これは特にドロップダウンメニューやモーダルウィンドウの実装に便利です。
x-effect
x-effect
ディレクティブは、データが変更されたときに処理を実行するディレクティブです。初期化時にも一度実行される点に注意してください。以下は、label
の値が変更されるたびにコンソールに出力されます。これはデバッグする際に便利です。
<html>
<head>
<title>Alpine.js CDN</title>
<!-- utf-8 -->
<meta charset="utf-8">
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
</head>
<body>
<div x-data="{ label: "Hello" }" x-effect="console.log(label)">
<button @click="label += " World!"">Change Message</button>
</div>
</body>
</html>

x-transition
x-transition
ディレクティブは、要素の表示・非表示時にアニメーションを付けるディレクティブです。以下は、要素の表示・非表示時にフェードイン・フェードアウトのアニメーションが付けられます。デフォルトはフェード・スケールが変更されて表示・非表示を行います。
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle</button>
<div x-show="open" x-transition>
Hello
</div>
</div>
より詳細な制御をしたい場合は、以下のような修飾子を使用できます:
<!-- エントリーアニメーション用のディレクティブ -->
<div
x-show="open"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-90"
x-transition:enter-end="opacity-100 scale-100"
>
<!-- 退出アニメーション用のディレクティブ -->
<div
x-show="open"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-90"
>
オプションとして duration
や delay
、opacity
、scale
などを指定することができます。
<div ... x-transition:enter.duration.500ms></div>
<div ... x-transition:leave.duration.400ms></div>
<div ... x-transition.delay.50ms></div>
<div ... x-transition.opacity></div>
<div ... x-transition.scale.80></div>
HTML表示
x-text
では、指定した文字列がそのまま表示されます。HTMLを使いたい場合には、x-html
を使います。ただし、ユーザー入力を直接表示する場合はXSS攻撃のリスクがあるため注意が必要です。
<div x-data="{ username: "<strong>calebporzio</strong>" }">
Username: <span x-html="username"></span>
</div>
x-model
入力欄を使う際には、x-model
を使って値を取得します。これは双方向バインディングを提供し、入力値が自動的にデータに反映され、逆にデータが変更されると入力欄の表示も更新されます。以下は、入力した値を取得して表示する例です。
<div x-data="{ message: "" }">
<input type="text" x-model="message">
<span x-text="message"></span>
</div>
x-model
はテキスト入力だけでなく、チェックボックスやラジオボタン、セレクトボックスなど様々な入力要素でも使えます:
<!-- チェックボックスの例 -->
<div x-data="{ isChecked: false }">
<input type="checkbox" x-model="isChecked">
<span x-show="isChecked">チェックされています!</span>
</div>
<!-- セレクトボックスの例 -->
<div x-data="{ selectedOption: "" }">
<select x-model="selectedOption">
<option value="">選択してください</option>
<option value="option1">オプション1</option>
<option value="option2">オプション2</option>
</select>
<span x-text="selectedOption"></span>
</div>
x-if
条件に応じて要素を表示する際には、x-if
を使って条件分岐を行います。x-show
との違いは、x-show
はCSSで表示・非表示を切り替えるのに対し、x-if
はDOM自体を追加・削除します。以下は、open
の値が true
の場合に要素を表示します。
<template x-if="open">
<div>Contents...</div>
</template>
x-for
リストを表示する際には、x-for
を使って繰り返し処理を行います。以下は、リストを表示する例です。表示する内容は <template />
タグで囲みます。
<ul x-data="{ colors: ["Red", "Orange", "Yellow"] }">
<template x-for="color in colors">
<li x-text="color"></li>
</template>
</ul>
オブジェクトの配列の場合は key
を指定します。これは効率的なDOMの更新とレンダリングのために重要です。
<ul x-data="{ colors: [
{ id: 1, label: "Red" },
{ id: 2, label: "Orange" },
{ id: 3, label: "Yellow" },
]}">
<template x-for="color in colors" :key="color.id">
<li x-text="color.label"></li>
</template>
</ul>
インデックスを使用したい場合は、次のようにします:
<template x-for="(color, index) in colors" :key="index">
<li x-text="${index + 1}: ${color}
"></li>
</template>
x-ignore
x-ignore
ディレクティブは、要素をAlpine.jsの処理から除外するディレクティブです。以下は、x-ignore
が指定された要素はAlpine.jsによって処理されません。これは他のJavaScriptライブラリと競合する部分がある場合に便利です。
<div x-data="{ label: "From Alpine" }">
<div x-ignore>
<!-- 以下は無視されます -->
<span x-text="label"></span>
</div>
</div>
x-ref
x-ref
ディレクティブは、要素を参照するディレクティブです。これによりDOMを直接操作することができます。以下は、ボタンを押すと x-ref
で指定した text
のDOMを削除します。
<button @click="$refs.text.remove()">Remove Text</button>
<span x-ref="text">Hello </span>
追加のディレクティブと修飾子
x-cloak
ページの読み込み中にテンプレートが一瞬表示されるのを防ぐためのx-cloak
ディレクティブです。CSSと組み合わせて使用します:
<style>
[x-cloak] { display: none !important; }
</style>
<div x-data="{ ready: false }" x-cloak>
<!-- ページが完全に読み込まれるまで表示されません -->
</div>
x-init
コンポーネントの初期化時に実行されるコードを指定できます:
<div x-data="{ message: "Hello" }" x-init="console.log("Component initialized")">
<span x-text="message"></span>
</div>
イベント修飾子
Alpine.jsでは、イベントハンドラに様々な修飾子を追加できます:
<!-- Enter キーが押されたときだけ発火 -->
<input @keyup.enter="submitForm">
<!-- 右クリック時のイベント -->
<button @click.right="showContextMenu">右クリック</button>
<!-- イベントの伝播を止める -->
<button @click.stop="handleClick">クリック</button>
<!-- デフォルトの挙動を防ぐ -->
<a href="/some-url" @click.prevent="doSomething">リンク</a>
<!-- 一度だけ実行 -->
<button @click.once="doOnce">一度だけ</button>
その他のTips
DOMの更新を待ってから処理をする
DOMの更新を待ってから処理をするには、$nextTick
を使います。これはUIが更新された後に何か処理を行いたい場合に便利です。以下は、ボタンを押すと message
の値が変更され、その後に console.log
が実行されます。
<div x-data="{ message: "Hello" }">
<button @click="
message = "World";
$nextTick(() => console.log(message))">
Change Message
</button>
</div>
ネットワーク処理を行う
ネットワーク処理を行うには、async/await
を使います。Alpine.jsでは、非同期処理をシンプルに扱うことができます。
<script>
// JavaScriptで、非同期処理を行う関数を定義
async function getLabel() {
let response = await fetch("/api/label");
return await response.text();
}
</script>
<!-- Alpine.js側の記述 -->
<div x-data>
<span x-text="await getLabel()"></span>
</div>
Alpine.jsでは、プロパティに関数を割り当て、最後のカッコを外すことで、自動的にasync関数であると判定し、await
を省略できます。この機能は便利ですが、使い方に注意が必要です。
<div x-data="{
getLabel: async function() {
let response = await fetch("/api/label");
return await response.text();
}
}">
<span x-text="getLabel"></span>
</div>
データ監視
$watch
を使って、データの変更を監視することができます:
<div x-data="{ count: 0 }" x-init="$watch("count", value => console.log(value))">
<button @click="count++">増加</button>
</div>
API
Alpine.jsのオブジェクトに対して、JavaScriptから操作もできます。より複雑なUI/UXを実現する際に使えそうです。
$store
$store
は、Alpine.jsのグローバルなデータストアにアクセスするためのオブジェクトです。以下は、ダークモードの切り替えを行う例です。
<button x-data @click="$store.darkMode.toggle()">ダーク・ライトモードの切り替え</button>
<div x-data :class="$store.darkMode.on && "bg-black"">
<!-- HTMLのコンテンツ -->
</div>
<script>
document.addEventListener("alpine:init", () => {
Alpine.store("darkMode", {
on: false, // デフォルトはライトモード
toggle() { // ダーク・ライトモードの切り替え
this.on = ! this.on;
},
});
});
</script>
$dispatch
カスタムイベントを発火させるための $dispatch
メソッドも用意されています:
<div x-data>
<button @click="$dispatch("custom-event", { message: "Hello!" })">
イベント発火
</button>
</div>
<div x-data @custom-event="alert($event.detail.message)">
<!-- カスタムイベントをリッスン -->
</div>
$root
$root
は、Alpine.jsのルート要素を取得するオブジェクトです。以下は、ルート要素のデータを取得してアラートを表示する例です。
<div x-data data-message="Hello World!">
<button @click="alert($root.dataset.message)">Say Hi</button>
</div>
Alpine.data()
コンポーネントのロジックを再利用するための Alpine.data()
メソッドも提供されています:
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("dropdown", () => ({
open: false,
toggle() {
this.open = !this.open;
},
close() {
this.open = false;
}
}));
});
</script>
<div x-data="dropdown">
<button @click="toggle">トグル</button>
<div x-show="open" @click.outside="close">ドロップダウン内容</div>
</div>
簡単なTodoアプリの例
以下はAlpine.jsだけを使って作成した、簡単なTodoアプリの例です。JavaScriptは使わずに、Alpine.jsだけで実装しています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alpine.js Todoアプリ</title>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: #2c3e50;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 8px;
background-color: #ecf0f1;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
button {
background-color: #e74c3c;
border: none;
padding: 4px 8px;
color: white;
cursor: pointer;
}
button:hover {
background-color: #c0392b;
}
</style>
</head>
<body>
<div x-data="{ newTodo: "", todos: [], addTodo() { if (this.newTodo.trim()) { this.todos.push(this.newTodo.trim()); this.newTodo = ""; } }, removeTodo(index) { this.todos.splice(index, 1); } }">
<h1>Alpine.js Todoアプリ</h1>
<input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
<button @click="addTodo">追加</button>
<ul>
<template x-for="(todo, index) in todos" :key="index">
<li>
<span x-text="todo"></span>
<button @click="removeTodo(index)">削除</button>
</li>
</template>
</ul>
</div>
</body>
</html>

長いx-dataブロックに改行を入れると動作に問題が生じることがあります。これはHTMLの属性値にある特殊文字や改行の扱いによるものです。この問題を回避するためには、次のように外部関数を使うのがお勧めです:
<div x-data="todoApp()">
<h1>Alpine.js Todoアプリ</h1>
<input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
<button @click="addTodo">追加</button>
<ul>
<template x-for="(todo, index) in todos" :key="index">
<li>
<span x-text="todo"></span>
<button @click="removeTodo(index)">削除</button>
</li>
</template>
</ul>
</div>
<script>
function todoApp() {
return {
newTodo: "",
todos: [],
addTodo() {
if (this.newTodo.trim()) {
this.todos.push(this.newTodo.trim());
this.newTodo = "";
}
},
removeTodo(index) {
this.todos.splice(index, 1);
}
}
}
</script>
もっと高度な例として、ローカルストレージにTodoを保存するバージョンを考えてみましょう:
<div x-data="persistentTodoApp()">
<h1>永続化Todoアプリ</h1>
<input x-model="newTodo" @keyup.enter="addTodo" placeholder="新しいタスクを入力">
<button @click="addTodo">追加</button>
<ul>
<template x-for="(todo, index) in todos" :key="index">
<li>
<span x-text="todo.text"></span>
<div>
<button @click="toggleCompleted(index)" x-text="todo.completed ? "完了" : "未完了""
:style="todo.completed ? "background-color: green;" : """></button>
<button @click="removeTodo(index)">削除</button>
</div>
</li>
</template>
</ul>
</div>
<script>
function persistentTodoApp() {
return {
newTodo: "",
todos: [],
init() {
this.todos = JSON.parse(localStorage.getItem("todos") || "[]");
this.$watch("todos", () => {
localStorage.setItem("todos", JSON.stringify(this.todos));
});
},
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = "";
}
},
toggleCompleted(index) {
this.todos[index].completed = !this.todos[index].completed;
},
removeTodo(index) {
this.todos.splice(index, 1);
}
}
}
</script>
Alpine.jsディレクティブ一覧
ディレクティブ | 説明 | 使用例 |
---|---|---|
x-data |
コンポーネントのデータを定義 | <div x-data="{ count: 0 }"> |
x-text |
要素のテキスト内容をバインド | <span x-text="count"></span> |
x-html |
要素のHTML内容をバインド(XSS注意) | <div x-html="username"></div> |
x-show |
条件に応じて要素の表示/非表示を切り替え | <div x-show="open">内容...</div> |
x-if |
条件に応じてDOM要素を追加/削除 | <template x-if="open"><div>内容...</div></template> |
x-for |
配列要素を繰り返し表示 | <template x-for="item in items" :key="item.id"><li x-text="item.name"></li></template> |
x-model |
フォーム要素との双方向バインディング | <input type="text" x-model="message"> |
x-on (@ ) |
イベントリスナーを設定 | <button @click="count++">クリック</button> |
x-transition |
要素の表示/非表示時のアニメーション | <div x-show="open" x-transition>内容...</div> |
x-effect |
データ変更時に処理を実行(初期化時も実行) | <div x-effect="console.log(message)"> |
x-ref |
要素への参照を作成 | <span x-ref="text">Hello</span> |
x-cloak |
初期化前の要素を非表示 | <div x-cloak>Loading...</div> |
x-init |
コンポーネント初期化時に処理を実行 | <div x-init="$el.textContent = 'Ready'"> |
x-ignore |
Alpine.jsの処理から要素を除外 | <div x-ignore><span x-text="label"></span></div> |
イベント修飾子
修飾子 | 説明 | 使用例 |
---|---|---|
.prevent |
デフォルトのイベント動作を防止 | <form @submit.prevent="handleSubmit"> |
.stop |
イベントの伝播を停止 | <button @click.stop="handleClick"> |
.outside |
要素外のクリックを検知 | <div @click.outside="open = false"> |
.window |
ウィンドウレベルのイベントをリッスン | <div @scroll.window="handleScroll"> |
.once |
イベントを一度だけ実行 | <button @click.once="doThis"> |
.self |
イベントが要素自体から発生した場合のみ実行 | <div @click.self="handleClick"> |
.enter |
Enterキー押下時のみ実行 | <input @keyup.enter="submit"> |
.escape |
Escapeキー押下時のみ実行 | <input @keyup.escape="close"> |
マジックプロパティ
プロパティ | 説明 | 使用例 |
---|---|---|
$el |
現在の要素への参照 | <button @click="$el.textContent = 'Clicked'"> |
$refs |
x-refで定義した要素への参照 | <button @click="$refs.text.remove()"> |
$event |
現在のDOMイベントオブジェクト | <button @click="console.log($event.target)"> |
$dispatch |
カスタムイベントを発火 | <button @click="$dispatch('custom-event')"> |
$nextTick |
DOM更新後に処理を実行 | <button @click="$nextTick(() => { ... })"> |
$watch |
プロパティ変更を監視 | <div x-init="$watch('count', value => ...)"> |
$store |
グローバルストアにアクセス | <div x-text="$store.user.name"> |
まとめ
Alpine.jsはちょっとしたインタラクティブなUI/UXであれば、JavaScriptを書かずに実現できます。また、さらに複雑な場合にも、JavaScriptからAlpine.jsのAPIを使って操作することができます。
Alpine.jsの主な利点は:
- 軽量:ファイルサイズが小さく、パフォーマンスへの影響が少ない
- 学習コストが低い:Vue.jsなどに似た構文で、シンプルで直感的
- プログレッシブエンハンスメント:既存のHTMLに簡単に追加できる
- 依存関係なし:他のライブラリに依存せず単独で動作する
より複雑なアプリケーションには、Vue.jsやReactなどのフレームワークが適している場合もありますが、ちょっとしたインタラクティブ要素や、既存のサイトへの機能追加には、Alpine.jsが最適な選択肢になり得ます。
ぜひ、Alpine.js公式サイトも参照しながら、試してみてください。