Alpine.jsでドロップダウンメニューをスクラッチから作成(Alpine.jsの基礎)

Alpine.jsはドロップダウンメニューやタブの切り替え、モーダルなどの機能をVue.jsやReactなど利用せず手軽に実装したい場合に利用できるJavaScriptのフレームワークです。実際に利用してみるとその手軽さを実感することができます。HTMXと組み合わせて利用されるなどAlpine.jsが使用されるケースが増えて現在も積極的に更新が行われています。

本文書ではLaravelフレークワークでも利用されているドロップダウンメニューの作成に必要なAlpine.jsの基礎を説明しスクラッチからドロップダウンメニューを作成します。
目次
Alpine.jsを使うための準備
任意のディレクトリにindex.htmlファイルを作成してcdnを使ってalpine.jsをロードします。これでAlpline.jsを使うための準備は完了です。何か特別な環境を構築することなく手元の環境で利用することができます。既存のhtmlファイルにちょっとした動きを入れたい時にも利用することができます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
初めてAlpine.js
Alpine.jsを使ってindex.htmlファイルにあるh1タグで表示されているHello Worldを表示させてみましょう。
divタグを追加しx-data属性を設定します。Alpine.jsのドキュメントでは”x-dataは新しいコンポーネントスコープを宣言します”と記述されていますがもし意味がわからなければAlpine.jsを利用するために必須な宣言と理解しておきましょう。x-dataの中では、messageにHello Worldを設定しているのでx-dataを設定したdivの内側であればこのmessageプロパティを自由に利用することができます。
<body>
<div x-data="{message: 'Hello World'}"></div>
</body>
messageをブラウザに表示させるためにx-textディレクティブを追加します。x-textにmessageを設定することでh1タグにmessageの内容が挿入され表示されます。
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
</div>
ブラウザで確認するとmessageに設定されたHello Worldが表示されます。

下記のようにh1タグの中にx-dataとx-textを記述することもできます。
<h1 x-data="{message: 'Hello World'}" x-text="message"></h1>
x-dataのdivの内側(スコープの範囲)であれば複数のv-textを利用してもmessageを表示させることができますがその範囲外になるとv-textは利用できません。
<body>
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
<h1 x-text="message"></h1>
<h1 x-text="message"></h1>
</div>
<h1 x-text="message"></h1>
</body>
ブラウザで見るとスコープ内の3つのHello Worldが表示されスコープの範囲を超えたx-textは表示されないことが確認できます。

さらに1つのHTMLページの中に複数のx-dataを宣言することも可能です。messageに異なる値を設定しています。
<body>
<div x-data="{message: 'Hello World'}">
<h1 x-text="message"></h1>
</div>
<div x-data="{message: 'Hello Alpine.js'}">
<h1 x-text="message"></h1>
</div>
</body>

messageに対してStringとしてJavaScriptのメソッドを実行することも可能です。
<div x-data="{message: 'Hello World'}">
<h1 x-text="message.split('').reverse().join('')"></h1>
</div>

clickイベントの設定
clickイベントを利用して他の処理を行うことができます。ここではclickイベントを検知してmessageの文字列を別の文字列で上書きします。button要素を追加しclickイベントに対してイベントリスナーを設定します。clickイベントを設定する時はx-on:をclickの前に追加します。
<div x-data="{message: 'Hello World'}">
<h1 x-text="message.split('').reverse().join('')"></h1>
<button x-on:click="message = 'Hello Alpine.js'">クリック</button>
</div>
最初はHello Worldが逆に表示されていますがボタンをクリックするとクリックイベントによりmessageがHello Alpine.jsで上書きされます。

クリックイベントの設定をx-on:clickと記述しましたが@clickと記述することもできます。clickイベントを利用することでユーザはインタラクティブな処理を行うことができるようになります。
<button @click="message = 'Hello Alpine.js'">クリック
clickイベントとx-showの設定
clickイベントを利用することで要素の表示・非表示に利用することができます。表示・非表示といえば思い浮かぶのはドロップダウンメニューやモーダルなどではないでしょうか。
Alpine.jsでは要素の表示・非表示はx-showディレクティブを利用して制御します。x-showの値にはtrue or falseのbooleanを設定します。x-showの値がtrueであれば表示、falseであれば非表示となります。
x-dataの中でisShowプロパティを設定しtrueとします。h1タグにx-show=”isShow”を設定していますがisShowがfalseなのでh1タグの内容(Hello World)は表示されず画面は真っ白の状態です。
<div x-data="{isShow: false}">
<h1 x-show="isShow">Hello World</h1>
</div>
isShowの値をfalseからtrueに変更するとHello Worldがブラウザに表示されます。
<div x-data="{isShow: true}">
<h1 x-show="isShow">Hello World</h1>
</div>
clickイベントを使ってmessageの文字列を上書きできたようにisShowの値を更新できるようにclickイベントをボタンに設定します。ボタンを使ってisShowの値をtrueからfalse、falseからtrueに切り替えれるように設定を行っています。
<div x-data="{isShow: false}">
<h1 x-show="isShow">Hello World</h1>
<button @click="isShow = !isShow">表示・非表示</button>
</div>
最初はisShowがfalseなのでボタンのみが表示されている状態です。

”表示・非表示”ボタンをクリックするとHello Worldが表示されます。

ボタンにv-textと三項演算子を利用することでisShowの値によってボタンに表示される文字も切り替えることができます。
<button @click="isShow = !isShow" x-text="isShow ? '非表示':'表示'"></button>
Alpine.jsでは特別の初期設定を行うこともなく数行のコードのシンプルなコードの設定だけで表示・非表示を繰り返すことができるToggle機能を実装することが可能です。

プラグイン
Alpine.jsではプラグインを利用することで機能を拡張することができます。ここではIntersectプログインを利用してプラグインの設定とIntersectプラグインの利用方法を確認します。
Intersectプラグイン
Intersectプラグインを利用することでInterSection Observerを利用することができます。InterSection Observerは指定したある要素がViewPortに入ってきた時に要素が入ったことを検知し事前に設定していた処理を実行することができます。例えばスクロールを行いViewportに入ってきた画像要素を検知してアニメーションを設定するといったことが可能です。
利用するためにプラグインなのでcdnでIntersectプラグインに関係するファイルを読み込む必要があります。
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
x-intersectディレクティブをx-dataのスコープの範囲の内部の監視したい要素に設定して処理を設定します。下記ではx-intersectでshow変数をtrueに更新しています。x-transition.duration.2000msを設定しているので最初非表示になっている要素がViewportに入ると2秒かけて表示されます。一度表示されると非表示になることはありません。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Alpine Plugins -->
<script
defer
src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"
></script>
<!-- Alpine Core -->
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<title>Document</title>
</head>
<body>
<div class="m-8">
<h1>Intersect Plugin</h1>
<p style="margin-bottom: 1000px">Scroll</p>
<div x-data="{ shown: false }" x-intersect="shown = true">
<div x-show="shown" x-transition.duration.2000ms>
I'm in the viewport!
</div>
</div>
</div>
</body>
</html>
I’m in the viewportを文字列から画像に変更するとスクロールして画像の要素がViewportに入るとゆっくりと画像が表示されるアニメーションを作成することができます。ホームページで頻繁に利用されているアニメーションもalpine.jpを利用することで簡単に実装することができることがわかりました。
さらにx-intersect:leaveを利用することで一度表示した”I’m in the viewport”がViewPortから外れた時に非表示にすることもできます。
<body>
<div class="m-8">
<h1>Intersect Plugin</h1>
<p style="margin-bottom: 1000px">Scroll</p>
<div
x-data="{ shown: false }"
x-intersect="shown = true"
x-intersect:leave="shown = false"
>
<div x-show="shown" x-transition.duration.2000ms>
I'm in the viewport!
</div>
</div>
</div>
</body>
x-intersect:enterと設定するとx-intersectと同じ設定となります。leaveを設定した場合にenterを設定することで違いがわかりやすくなります。
Modifiersの設定
x-intersectとx-intersect:leaveを利用することでViewPortに要素に入った時、出た時に繰り返し表示・非表示を設定することができましたが一度だけ表示させたい場合にはModifiersのonceを利用することができます。
<body>
<div class="m-8">
<h1>Intersect Plugin</h1>
<p style="margin-bottom: 1000px">Scroll</p>
<div
x-data="{ shown: false }"
x-intersect.once="shown = true"
x-intersect:leave="shown = false"
>
<div x-show="shown" x-transition.duration.2000ms>
I'm in the viewport!
</div>
</div>
</div>
</body>
一度下にスクロールして表示された後、上にスクロールして非表示にします。その後スクロールしても表示されることはありません。
実践編ドロップダウンメニューを作成
ここまでのAlpine.jsの機能を利用してAlpine.jsが頻繁に利用させるドロップダウンメニューを作成することができます。Step By Stepでドロップダウンメニューを作成していきますがCSSにはTailwind CSSを利用します。Tailwind CSSを使うとJavaScriptにおけるAlpine.jsのように手軽にCSSを設定することができます。
cdnを経由してTailwind CSSを読み込むので以下のlinkタグをheaderタグに追加してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Alpine.js CDN -->
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<title>Alpine.js DropDown Menu</title>
</head>
<body>
//略
</body>
</html>
ドロップダウンメニューをスクラッチから作成といってもほとんどの時間はドロップダウンメニューのCSSの設定に費やします。Alpine.jsを設定する処理はわずかです。それぐらいAlpine.jsは手軽に利用することができます。
ヘッダーとメインコンテンツ領域を作成
ヘッダーの背景色は黒に設定し、classでh-20の高さを設定します。
<body>
<header class="bg-black h-20">
</header>
<main class="m-4">
<p>ここからがコンテンツ</p>
</main>
</body>

左側にログ、右側にドロップダウンメニューを表示させるためにFlexboxを利用します。両端に表示させるためflex justify-betweenを設定し、items-centerで中央揃えにしています。
<body>
<header class="bg-black h-20 flex items-center justify-between p-4">
<div class="text-white font-bold">ロゴ</div>
<div>
<button class="text-white">ドロップダウンメニュー</button>
</div>
</header>
<main class="m-4">
<p>ここからがコンテンツ</p>
</main>
</body>

ドロップダウンにメニューを追加
ドロップダウンにメニューを追加します。3つのメニューをaタグで追加しています。
<div>
<button class="text-white">ドロップダウンメニュー</button>
<div>
<a href="#">Link1</a>
<a href="#">Link2</a>
<a href="#">Link3</a>
</div>
</div>
aタグはインライン要素なので横並びに表示されていますが背景色が黒でフォントの色もデフォルトの黒なので何も表示されませんがドロップメニューの下には存在しています。

メニューをインライン要素からブロック要素に変更します。メニューの外側のdivに背景色の白を設定します。
<div>
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white">
<a href="#" class="block">Link1</a>
<a href="#" class="block">Link2</a>
<a href="#" class="block">Link3</a>
</div>
</div>
メニューが表示されるようになりました。

メニューにabsoluteを設定し基準となる要素である外側の要素にrelativeを設定します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute">
<a href="#" class="block">Link1</a>
<a href="#" class="block">Link2</a>
<a href="#" class="block">Link3</a>
</div>
</div>
ドロップダウンメニューが開い時の状態に近づいてきました。

padding、margin、メニューの横幅、表示位置が右側の0が記述になるように変更します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1">
<a href="#" class="block p-2">Link1</a>
<a href="#" class="block p-2">Link2</a>
<a href="#" class="block p-2">Link3</a>
</div>
</div>
横幅やmargin, paddingを設定したのでよりドロップダウンメニューらしくなりました。

メニューと背景の境界がわかりくいのでshadowを設定し、hoverを設定してマウスが上にきた時は背景色が変わるように設定します。
<div class="relative">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>

ここまででドロップダウンメニューのCSS設定は完了です。
Alpine.jsの設定
ドロップダウンメニューをクリックするとメニューが表示・非表示を切り替えられるようにAlpine.jsの設定を行います。表示・非表示の切り替えは確認済みです。
ドロップダウンメニューの外側のdiv要素にx-dataを追加してisOpenプロパティを追加します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
メニューの外側のdiv要素にx-showでisOpenを設定します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md" x-show="isOpen">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
x-dataのisOpenの値がfalseの場合のみメニューが非表示になるか確認してください。次はtrueにしてメニューが表示されればAlpine.jsは正常に動作しています。
ドロップダウンメニューをクリックすると表示・非表示が切り替わるようにclickイベントを設定します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
<div class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md" x-show="isOpen">
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
最初はメニューが閉じているのでクリックすると表示されます。

再度クリックするとドロップダウンメニューが非表示になります。

クリックした際にボタンの外側にラインが表示されるので表示しないようにbutton要素にoutline-noneのclassを設定します。
<div class="relative" x-data="{ isOpen:false }">
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">ドロップダウンメニュー</button>
away modifierの追加
設定したドロップダウンメニューはドロップダウンメニューボタンをクリックすると表示・非表示が切り替わります。x-dataの範囲外でクリックするとメニューが閉じるようにclickにaway modifierを設定します。
<div
class="relative"
x-data="{ isOpen:false }"
@click.away="isOpen = false"
>
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">
ドロップダウンメニュー
</button>
開いた状態でドロップダウンメニュー以外の領域をクリックするとメニューが閉じます。

そのほかに@keydown.escapeを設定するとESCキーを押してもメニューを閉じさせることができます。
<div
class="relative"
x-data="{ isOpen:false }"
@click.away="isOpen = false"
@keydown.escape="isOpen = false"
>
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">
ドロップダウンメニュー
</button>
Alpine.jsを使ってドロップメニューの完成です。CSSの調整に時間がかかるだけでAlpine.jsの設定はほとんどなく簡単にドロップダウンメニューが作成できることができました。
アニメーションの設定
ドロップダウンメニューをクリックすると表示・非表示が即座に行われるのでアニメーションを設定してスムーズに切り替えが行えるようにします。x-trantisionディレクティブを利用しますが例がhttps://github.com/alpinejs/alpineに公開されているのでそれらの値を利用します。x-showディレクティブが設定されている要素に追加します。
<div
class="relative"
x-data="{ isOpen:false }"
@click.away="isOpen = false"
@keydown.escape="isOpen = false"
>
<button class="text-white focus:outline-none" @click="isOpen = !isOpen">
ドロップダウンメニュー
</button>
<div
class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md"
x-show="isOpen"
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"
>
<a href="#" class="block p-2 hover:bg-gray-100">Link1</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link2</a>
<a href="#" class="block p-2 hover:bg-gray-100">Link3</a>
</div>
</div>
設定後はゆっくりとメニューが表示・非表示されることが確認できます。
x-transtionディレクティブを複数記述しなくても細かな設定必要ない場合は以下のように設定することも可能です。2秒をかけてゆっくりと表示・非表示されます。
<div
class="bg-white absolute w-56 right-0 my-2 py-1 shadow-lg rounded-md"
x-show="isOpen"
x-transition.duration.2000ms
>
設定方法についてはドキュメントにも記載されていますがここにも貼り付けておきます。各値を変更することで最適な値を見つけてください。
下記の一覧はAlpine.jsのバージョン3ではx-show.transitionからx-transitionに変更になりました。

以上でアニメーションを入れたドロップダウンメニューのAlpine.jsの設定は完了です。
ドロップダウンメニューを作成を通してAlpine.jsの手軽さが実感できたのではないでしょうか。