ReactやVueでアプリケーションを開発している場合にドロップダウンメニューやダイアログ(モーダル)を実装したいという時に役に立つのがHeadless UIです。一度設定を行ってしまえばHeadless UIがどんなものか理解することができると思うのですが、まだ設定を行っていない段階でHeadless UIのドキュメントに記載されている概要説明”Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.”を読んだだけではあまりよくわからない人も多いかと思います。この文章の意味を完全に理解することができればHeadless UIを使いこなすことができます。

Headless UIで利用できる機能(ドロップダウンメニューやダイアログなど)は事前に用意されているコンポーネントタグを利用するだけで実装することが可能です。しかしスタイルが全く適用されていないのでTailwind CSSを利用して自分で設定を行うことでオリジナルデザインのものを作成することができます。自由にスタイルを設定を行えるだけではなくユーザのアクセスビリティも考慮に入れ実装されているので開いたドロップダウンメニューをEscキーを使うだけで閉じることができたり、ドロップダウンメニューは上下キーを押すことでマウスを使うことなくメニューの移動することができます。

実際にVite環境のReact, Vueを利用してHeadless UIに使い方を学んだ後に”Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.”の意味がスッキリ理解できることを目的に説明を行なっていきます。

Headless UIは、Vueの場合はVue 3のみサポートしています。Headless UIはTailwind CSSを開発しているTailwind
Labsが開発しています。

Viteによるプロジェクトの作成

Reactであればcreate-app-react, vueであればVue CLIを利用してプロジェクトを作成することができますが最近利用するユーザも増えてきているViteを利用してプロジェクトの作成を行います。

Reactの場合

Viteでプロジェクトを作成するためにnpm init vite@latestコマンドを実行します。コマンドを実行すると対話的にプロジェクト名やフレームワーク、TypeScriptを利用するかどうか確認されるので設定を行ってください。


 % npm init vite@latest
Need to install the following packages:
  create-vite@latest
Ok to proceed? (y) y
✔ Project name: … headless-ui-react
✔ Select a framework: › react
✔ Select a variant: › react

Scaffolding project in  /...

Done. Now run:

  cd headless-ui-react
  npm install
  npm run dev

作成されたプロジェクトフォルダに移動してnpm installコマンドを実行してJavaScriptライブラリをインストールしてnpm run devで開発サーバを起動してください。


 % cd headless-ui-react 
 % npm install
 % npm run dev
 //略
   vite v2.7.3 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

ブラウザでhttp://localhost:3000/にアクセスすると下記の画面が表示されます。

Vite Reactの初期ページ
Vite Reactの初期ページ

Vueの場合

Viteでプロジェクトを作成するためにnpm init vite@latestコマンドを実行します。コマンドを実行すると対話的にプロジェクト名やフレームワーク、TypeScriptを利用するかどうか確認されるので設定を行ってください。


 % npm init vite@latest
✔ Project name: … headless-ui-vue
✔ Select a framework: › vue
✔ Select a variant: › vue

Scaffolding project in  /...

Done. Now run:

  cd headless-ui-vue
  npm install
  npm run dev

作成されたプロジェクトフォルダに移動してnpm installコマンドを実行してJavaScriptライブラリをインストールしてnpm run devで開発サーバを起動してください。


 % cd headless-ui-react 
 % npm install
 % npm run dev
 //略
   vite v2.7.3 dev server running at:

  > Local: http://localhost:3000/
  > Network: use `--host` to expose

ブラウザでhttp://localhost:3000/にアクセスすると下記の画面が表示されます。

Vite Vueの初期画面
Vite Vueの初期画面

TailwindCSSのインストール

Headless UIのスタイリングにTailwind CSSを利用するためインストールを行います。

Reactの場合


 % npm install -D tailwindcss postcss autoprefixer

設定ファイルを作成するためにinitコマンドを実行します。


 % npx tailwindcss init -p
Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

tailwind.config.jsファイルを開いて下記の設定を行います。


module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

index.cssを開いてtailwindのディレクティブを設定します。これまで記述されていた内容は上書きします。


@tailwind base;
@tailwind components;
@tailwind utilities;

インストール、設定が正常に完了しているか確認するためにApp.jsxファイルを更新します。


function App() {
  return <h1 className="text-3xl font-bold underline">Hello world!</h1>;
}

export default App;

太文字で下線が引かれたHello Worldが画面上に表示されればTawilwind CSSの初期設定は完了です。

Tailwind CSSの動作確認 React
Tailwind CSSの動作確認 React

Vueの場合


 % npm install -D tailwindcss postcss autoprefixer

設定ファイルを作成するためにinitコマンドを実行します。


 % npx tailwindcss init -p
Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js

tailwind.config.jsファイルを開いて下記の設定を行います。


module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

srcフォルダにindex.cssファイルを作成してtailwindのディレクティブを設定します。


@tailwind base;
@tailwind components;
@tailwind utilities;

main.jsファイルを開いた作成したindex.cssファイルをimportします。


import { createApp } from 'vue';
import App from './App.vue';
import './index.css';

createApp(App).mount('#app');

インストール、設定が正常に完了しているか確認するためにApp.vueファイルを更新します。


<template>
  <h1 class="text-3xl font-bold underline">Hello world!</h1>
</template>

太文字で下線が引かれたHello Worldが画面上に表示されればTawilwind CSSの初期設定は完了です。

Tailwind CSSの動作確認 Vue
Tailwind CSSの動作確認 Vue

Headless UIのインストール

Headless UIを利用するためにはライブラリのインストールが必要となります。React, Vueではインストールするライブラリが異なります。

Reactの場合


 % npm install @headlessui/react

Vueの場合


 % npm install @headlessui/vue

ドロップダウンメニューの設定(React)

インストールが完了したらHeadless UIを利用しない状態でのドロップダウンメニューを構成する要素を確認しておきます。Menuという名前のボタンをクリックするとHome, Service, About us, Contactの4つのメニューが表示されるものとします。

要素をhtmlのみで記述すると下記のようになります。


function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <div>
        <button>Menu</button>
        <ul>
          <li>
            <a href="/">Home</a>
          </li>
          <li>
            <a href="/service">Service</a>
          </li>
          <li>
            <a href="/aboutus">About us</a>
          </li>
          <li>
            <a href="/contact">Contact</a>
          </li>
        </ul>
      </div>
    </div>
  );
}

export default App;

ブラウザで確認するとスタイルが設定されていないので下記のように表示されます。

Headless UIを利用する前のドロップダウンメニューの構成する要素
Headless UIを利用する前のドロップダウンメニューの構成する要素

Menuコンポーネント

Headless UIではドロップダウンメニューを実装する場合はMenuコンポーネントを利用します。そのため@headlessui/reactからMenuをimportします。importしたMenuからMenu, Menu.Button, Menu.Items, Menu.Itemを利用します。先程のdivタグ、buttonタグ、ulタグ、liタグをMenuコンポーネントに置き換えます。


import { Menu } from '@headlessui/react';

function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <Menu> //div
        <Menu.Button>Menu</Menu.Button> //button
        <Menu.Items> //ul
          <Menu.Item> //li
            <a href="/">Home</a>
          </Menu.Item>
          <Menu.Item> //li
            <a href="/service">Service</a>
          </Menu.Item>
          <Menu.Item> //li
            <a href="/aboutus">About us</a>
          </Menu.Item>
          <Menu.Item> //li
            <a href="/contact">Contact</a>
          </Menu.Item>
        </Menu.Items>
      </Menu>
    </div>
  );
}

export default App;

ブラウザで確認するとMenuという文字は表示されていますが、それ以外のHome, Serviceなどは表示されていません。

Menuコンポーネント設定直後
Menuコンポーネント設定直後

Menuの文字列をクリックしてください。Menuの文字列の下にMenu.Itemで設定したHome, Serivce, About us, Contactが表示されます。

ボタンをクリックした後の表示
ボタンをクリックした後の表示

再度Menuをクリックすると表示されていたHome, Service, About us, Contactは非表示になります。

Menu.Itemの中にそのままHomeなどの文字列するとエラーが表示されます。

ここまでの設定で冒頭で説明した通り”Headless UIで利用できる機能(ドロップダウンメニューやダイアログなど)は事前に用意されているコンポーネントタグを利用するだけで実装することが可能です。しかしスタイルは全く適用されていないのでTailwind CSSを利用して自分で設定を行う必要があります。”が理解できたかと思います。

Headless UIを利用しない場合はuseStateを使ってopenなどの変数は定義して、clickイベントで開閉を設定するといったことが必要になりますがHeadless UIではコンポーネント内部で設定が行われているので改めて設定を行う必要があります。

propsの設定

機能の実装ができたのではpropsを利用してタグを設定します。デフォルトではMenu.Itemsにはdiv、Menu.buttonにはbutton、Menu、Menu.Itemにはfragmentが設定されています。

ulタグ、liタグを設定したい場合はas propsを利用することができます。Menu.Itemsコンポーネントにas propsでulを設定し、Menu.Itemコンポーネントにas propsでliを設定します。


<Menu.Items as="ul">
  <Menu.Item as="li">
    <a href="/">Home</a>
  </Menu.Item>
  <Menu.Item as="li">
    <a href="/service">Service</a>
  </Menu.Item>
  <Menu.Item as="li">
    <a href="/aboutus">About us</a>
  </Menu.Item>
  <Menu.Item as="li">
    <a href="/contact">Contact</a>
  </Menu.Item>
</Menu.Items>

ul, liが適用されているので先程まで横一列に並んでいた要素が縦並びになります。as propsによってコンポーネントには指定した要素タグが設定できることがわかりました。

メニュー一覧が縦表示
メニュー一覧が縦表示

スタイルの設定

スタイルを設定したい場合は通常の設定と同様にclassNameを利用して設定を行うことができます。classにはTailwind CSSを利用します。

ボタンにスタイルを設定したい場合はMenu.ButtonにclassName属性を使ってclassを適用することになります。


<Menu.Button className="px-4 py-2 border rounded-lg bg-blue-600 hover:bg-blue-400 text-white font-bold">
  Menu
</Menu.Button>
Menu.buttonにclassNameを設定
Menu.buttonにclassNameを設定

そのほかのコンポーネントにもスタイルを設定します。w-[200px]でドロップダウンメニューの幅を設定しています。Tailwind CSSではブラケットを利用して任意の値を設定することができます。divide-yを利用して子要素の区切り線を設定しています。


import { Menu } from '@headlessui/react';

function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <Menu>
        <Menu.Button className="px-4 py-2 border rounded-lg bg-blue-600 hover:bg-blue-400 text-white font-bold">
          Menu
        </Menu.Button>
        <Menu.Items as="ul" className="w-[200px] divide-y border rounded-lg mt-2">
          <Menu.Item as="li">
            <a href="/" className="p-2 inline-block">Home</a>
          </Menu.Item>
          <Menu.Item as="li">
            <a href="/service" className="p-2 inline-block">Service</a>
          </Menu.Item>
          <Menu.Item as="li">
            <a href="/aboutus" className="p-2 inline-block">About us</a>
          </Menu.Item>
          <Menu.Item as="li"">
            <a href="/contact" className="p-2 inline-block">Contact</a>
          </Menu.Item>
        </Menu.Items>
      </Menu>
    </div>
  );
}

export default App;
Menu.Items, Menu.Itemのスタイル適用
Menu.Items, Menu.Itemのスタイル適用

さらにアイコンを追加したい場合にはheroiconsなどを利用して設定を行うことができます。heroiconsを利用したい場合にはライブラリをインストールする必要があります。


 % npm install @heroicons/react

アイコンの設定を行います。


import { HomeIcon } from '@heroicons/react/outline';
//略
<Menu.Item as="li">
  <a href="/" className="p-2 inline-block flex items-center">
    <HomeIcon className="w-5 y-5 mr-2" />
    Home
  </a>
</Menu.Item>

その他のメニュー要素にもアイコンの設定を行います。


import { Menu } from '@headlessui/react';
import {
  HomeIcon,
  CogIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/react/outline';

function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <Menu>
        <Menu.Button className="px-4 py-2 border rounded-lg bg-blue-600 hover:bg-blue-400 text-white font-bold">
          Menu
        </Menu.Button>
        <Menu.Items
          as="ul"
          className="w-[200px] divide-y border rounded-lg mt-2"
        >
          <Menu.Item as="li">
            <a href="/" className="p-2 inline-block flex items-center">
              <HomeIcon className="w-5 y-5 mr-2" />
              Home
            </a>
          </Menu.Item>
          <Menu.Item as="li">
            <a href="/" className="p-2 inline-block flex items-center">
              <CogIcon className="w-5 y-5 mr-2" />
              Service
            </a>
          </Menu.Item>
          <Menu.Item as="li">
            <a href="/" className="p-2 inline-block flex items-center">
              <UsersIcon className="w-5 y-5 mr-2" />
              About Us
            </a>
          </Menu.Item>
          <Menu.Item as="li">
            <a href="/" className="p-2 inline-block flex items-center">
              <PhoneIcon className="w-5 y-5 mr-2" />
              Contact
            </a>
          </Menu.Item>
        </Menu.Items>
      </Menu>
    </div>
  );
}

export default App;

アイコンの表示されたメニューになります。

アイコン表示されたメニュー
アイコン表示されたメニュー

activeな要素へのスタイル設定

ドロップダウンのメニューの中で現在どのメニューがactiveかどうかはactive propsを利用して設定することができます。

activeの場合のみbg-blue-500で背景色を設定します。Homeは一番上のメニューでメニュー全体がborderで丸みをおびているためrounded-t-lgを設定しています。


<Menu.Item as="li" className="">
  {({ active }) => (
    <a
      className={`${
        active ? 'bg-blue-500 text-white' : 'bg-white text-black'
      } p-2 w-full inline-block flex items-center rounded-t-lg`}
      href="/"
    >
      <HomeIcon className="w-5 y-5 mr-2" />
      Home
    </a>
  )}
</Menu.Item>

その他のメニューも上記と同様にactiveの設定を追加します。ServiceとAbout usにrounded-XXの設定はありませんが一番の下のContactにはrounded-b-lgを設定しています。


import { Menu } from '@headlessui/react';
import {
  HomeIcon,
  CogIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/react/outline';

function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <Menu>
        <Menu.Button className="px-4 py-2 border rounded-lg bg-blue-500 hover:bg-blue-400 text-white font-bold">
          Menu
        </Menu.Button>
        <Menu.Items
          as="ul"
          className="w-[200px] divide-y border rounded-lg mt-2"
        >
          <Menu.Item as="li" className="">
            {({ active }) => (
              <a
                className={`${
                  active ? 'bg-blue-500 text-white' : 'bg-white text-black'
                } p-2 w-full inline-block flex items-center rounded-t-lg`}
                href="/"
              >
                <HomeIcon className="w-5 y-5 mr-2" />
                Home
              </a>
            )}
          </Menu.Item>
          <Menu.Item as="li">
            {({ active }) => (
              <a
                className={`${
                  active ? 'bg-blue-500 text-white' : 'bg-white text-black'
                } p-2 w-full inline-block flex items-center`}
                href="/service"
              >
                <CogIcon className="w-5 y-5 mr-2" />
                Service
              </a>
            )}
          </Menu.Item>
          <Menu.Item as="li">
            {({ active }) => (
              <a
                className={`${
                  active ? 'bg-blue-500 text-white' : 'bg-white text-black'
                } p-2 w-full inline-block flex items-center`}
                href="/about"
              >
                <UsersIcon className="w-5 y-5 mr-2" />
                About us
              </a>
            )}
          </Menu.Item>
          <Menu.Item as="li">
            {({ active }) => (
              <a
                className={`${
                  active ? 'bg-blue-500 text-white' : 'bg-white text-black'
                } p-2 w-full inline-block flex items-center rounded-b-lg`}
                href="/contact"
              >
                <PhoneIcon className="w-5 y-5 mr-2" />
                Contact
              </a>
            )}
          </Menu.Item>
        </Menu.Items>
      </Menu>
    </div>
  );
}

export default App;

マウスでメニューを選択するとそのメニューはactiveとなるため背景色が設定されます。

activeになったメニュー
activeになったメニュー

Menuボタンやメニューの外側の線がフォーカスにより下記のように太線になります。

メニューのボーダー線が太くなる場合

フォーカス時に表示される太線はMenu.ButtonとMenu.ItemsのclassNameにoutline-noneを追加することで表示されなくなります。


<Menu.Button className="px-4 py-2 border rounded-lg bg-blue-500 hover:bg-blue-400 text-white font-bold outline-none">
  Menu
</Menu.Button>
<Menu.Items
  as="ul"
  className="w-[200px] divide-y border rounded-lg mt-2 outline-none"
>

アクセスビィリティの確認

マウスをメニューの上に乗せると背景色が変わりますがMenuボタンを押した後、キーボードの↑(上)、↓(下)キーを押すことでメニューを変更することができます。またメニューが開いた後にメニュー以外の要素をクリックするとメニューが閉じる以外にESCキー、Spaceキーを押してもメニューを閉じることができます。ボタンがフォーカスされている時にSpaceキーを押すとメニューの開閉を行うことができます。

メニューボタンのフォーカスはMenu.Buttonにoutline-noneを設定しない場合に表示される太線が表示される時にフォーカスされた状態であることがわかります。

Headless UIではこのような設定が行われいてることが”fully accessible UI components”という意味に関連しています。

アニメーションの設定

メニューを表示・非表示にアニメーションを設定したい場合はTransitionコンポーネントを利用することができます。Transitionコンポーネントをheadlessui/reactからimportしてMenu.Itemsを包みます。Transtionコンポーネントに下記のようにアニメーション設定を行うことでふわっとメニューが表示されるアニメーションを簡単に実装することができます。


import { Menu, Transition } from '@headlessui/react';
//略
<Transition
  enter="transition duration-300 ease-out"
  enterFrom="transform scale-95 opacity-0"
  enterTo="transform scale-100 opacity-100"
  leave="transition duration-300 ease-out"
  leaveFrom="transform scale-100 opacity-100"
  leaveTo="transform scale-95 opacity-0"
>
  <Menu.Items
    as="ul"
    className="w-[200px] divide-y border rounded-lg mt-2 outline-none"
  >
  //略
  </Menu.Items>
</Transition>

ドロップダウンメニューの設定(Vue)

コードはComposition APIのscript setupを利用して記述していきます。

インストールが完了したらHeadless UIを利用しない状態でのドロップダウンメニューを構成する要素を確認しておきます。Menuという名前のボタンをクリックするとHome, Service, About us, Contactの4つのメニューが表示されるものとします。

要素をhtmlのみで記述すると下記のようになります。


<template>
  <div className="m-8">
    <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
    <div>
      <button>Menu</button>
      <ul>
        <li>
          <a href="/">Home</a>
        </li>
        <li>
          <a href="/service">Service</a>
        </li>
        <li>
          <a href="/aboutus">About us</a>
        </li>
        <li>
          <a href="/contact">Contact</a>
        </li>
      </ul>
    </div>
  </div>
</template>

ブラウザで確認するとスタイルが設定されていないので下記のように表示されます。

Headless UIを利用する前のドロップダウンメニューの構成する要素
Headless UIを利用する前のドロップダウンメニューの構成する要素

Menuコンポーネント

Headless UIではドロップダウンメニューを実装する場合はMenu, MenuButton, MenuItems, MenuItemsコンポーネントを利用します。これらのコンポーネントは@headlessui/vueからimportします。先程のdivタグをMenu、buttonタグをMenuButton、ulタグをMenuItems、liタグをMenuItemコンポーネントに置き換えます。


<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
</script>
<template>
  <div className="m-8">
    <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
    <Menu>
      <MenuButton>Menu</MenuButton>
      <MenuItems>
        <MenuItem>
          <a href="/">Home</a>
        </MenuItem>
        <MenuItem>
          <a href="/service">Service</a>
        </MenuItem>
        <MenuItem>
          <a href="/aboutus">About us</a>
        </MenuItem>
        <MenuItem>
          <a href="/contact">Contact</a>
        </MenuItem>
      </MenuItems>
    </Menu>
  </div>
</template>

ブラウザで確認するとMenuという文字は表示されていますが、それ以外のHome, Serviceなどは表示されていません。

Menuコンポーネント設定直後
Menuコンポーネント設定直後

Menuの文字列をクリックしてください。Menuの文字列の下にMenuItemコンポーネントで設定したHome, Serivce, About us, Contactが表示されます。

ボタンをクリックした後の表示
ボタンをクリックした後の表示

propsの設定

機能の実装ができたのではpropsを利用してタグを設定します。デフォルトではMenuItemsにはdiv、MenuButtonにはbutton、Menu, MenuItemには何も設定されています。

ulタグ、liタグを設定したい場合はas propsを利用することができます。MenuItemsコンポーネントにas propsでulを設定し、MenuItemコンポーネントにas propsでliを設定します。


<MenuItems as="ul">
  <MenuItem as="li">
    <a href="/">Home</a>
  </MenuItem>
  <MenuItem as="li">
    <a href="/service">Service</a>
  </MenuItem>
  <MenuItem as="li">
    <a href="/aboutus">About us</a>
  </MenuItem>
  <MenuItem as="li">
    <a href="/contact">Contact</a>
  </MenuItem>
</MenuItems>

ul, liが適用されているので先程まで横一列に並んでいた要素が縦並びになります。as propsによってコンポーネントには指定した要素タグが設定できることがわかりました。

メニュー一覧が縦表示
メニュー一覧が縦表示

スタイルの設定

スタイルを設定したい場合は通常の設定と同様にclass属性を利用して設定を行うことができます。classにはTailwind CSSを利用します。

ボタンにスタイルを設定したい場合はMenuButtonにclass属性を使ってclassを適用することになります。


<MenuButton
  class="
    px-4
    py-2
    border
    rounded-lg
    bg-blue-600
    hover:bg-blue-400
    text-white
    font-bold
  "
  >Menu</MenuButton
>
MenuButtonにclassを設定
MenuButtonにclassを設定

そのほかのコンポーネントにもスタイルを設定します。w-[200px]でドロップダウンメニューの幅を設定しています。Tailwind CSSではブラケットを利用して任意の値を設定することができます。divide-yを利用して子要素の区切り線を設定しています。


<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
</script>
<template>
  <div className="m-8">
    <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
    <Menu>
      <MenuButton
        class="
          px-4
          py-2
          border
          rounded-lg
          bg-blue-600
          hover:bg-blue-400
          text-white
          font-bold
        "
        >Menu</MenuButton
      >
      <MenuItems as="ul" class="w-[200px] divide-y border rounded-lg mt-2">
        <MenuItem as="li">
          <a href="/" className="p-2 inline-block">Home</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/service" className="p-2 inline-block">Service</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/aboutus" className="p-2 inline-block">About us</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/contact" className="p-2 inline-block">Contact</a>
        </MenuItem>
      </MenuItems>
    </Menu>
  </div>
</template>
MenuItems, MenuItemにclassでスタイル適用
MenuItems, MenuItemにclassでスタイル適用

さらにアイコンを追加したい場合にはheroiconsなどを利用して設定を行うことができます。heroiconsを利用したい場合にはライブラリをインストールする必要があります。


 % npm install @heroicons/vue

最初のメニューにホームアイコンを設定します。


<MenuItem as="li">
  <a href="/" class="p-2 inline-block flex items-center">
    <HomeIcon class="w-5 y-5 mr-2" />
    Home
  </a>
</MenuItem>

その他のメニュー要素にもアイコンの設定を行います。


<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
import {
  HomeIcon,
  CogIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/vue/outline';
</script>
<template>
  <div className="m-8">
    <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
    <Menu>
      <MenuButton
        class="
          px-4
          py-2
          border
          rounded-lg
          bg-blue-600
          hover:bg-blue-400
          text-white
          font-bold
        "
        >Menu</MenuButton
      >
      <MenuItems as="ul" class="w-[200px] divide-y border rounded-lg mt-2">
        <MenuItem as="li">
          <a href="/" class="p-2 inline-block flex items-center">
            <HomeIcon class="w-5 y-5 mr-2" />
            Home
          </a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/" class="p-2 inline-block flex items-center">
            <CogIcon class="w-5 y-5 mr-2" />
            Service
          </a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/" class="p-2 inline-block flex items-center">
            <UsersIcon class="w-5 y-5 mr-2" />
            About Us
          </a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/" class="p-2 inline-block flex items-center">
            <PhoneIcon class="w-5 y-5 mr-2" />
            Contact
          </a>
        </MenuItem>
      </MenuItems>
    </Menu>
  </div>
</template>

アイコンの表示されたメニューが表示されます。

アイコン表示されたメニュー
アイコン表示されたメニュー

activeな要素へのスタイル設定

ドロップダウンのメニューの中で現在どのメニューがactiveかどうかはv-slotを利用して設定することができます。

activeの場合のみbg-blue-500で背景色を設定します。Homeは一番上のメニューでメニュー全体がborderで丸みをおびているためrounded-t-lgを設定しています。


<MenuItem as="li" v-slot="{ active }">
  <a
    href="/"
    :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
    class="p-2 inline-block flex items-center"
  >
    <HomeIcon class="w-5 y-5 mr-2" />
    Home
  </a>
</MenuItem>

その他のメニューも上記と同様にv-slotとactiveの設定を追加します。ServiceとAbout usにrounded-XXの設定はありませんが一番の下のContactにはrounded-b-lgを設定しています。


<script setup>
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue';
import {
  HomeIcon,
  CogIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/vue/outline';
</script>
<template>
  <div className="m-8">
    <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
    <Menu>
      <MenuButton
        class="
          px-4
          py-2
          border
          rounded-lg
          bg-blue-600
          hover:bg-blue-400
          text-white
          font-bold
        "
        >Menu</MenuButton
      >
      <MenuItems as="ul" class="w-[200px] divide-y border rounded-lg mt-2">
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 inline-block flex items-center"
          >
            <HomeIcon class="w-5 y-5 mr-2" />
            Home
          </a>
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 inline-block flex items-center"
          >
            <CogIcon class="w-5 y-5 mr-2" />
            Service
          </a>
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 inline-block flex items-center"
          >
            <UsersIcon class="w-5 y-5 mr-2" />
            About Us
          </a>
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 inline-block flex items-center"
          >
            <PhoneIcon class="w-5 y-5 mr-2" />
            Contact
          </a>
        </MenuItem>
      </MenuItems>
    </Menu>
  </div>
</template>

マウスでメニューを選択するとそのメニューはactiveとなるため背景色が設定されます。

activeの場合のメニュー
activeの場合のメニュー

Menuボタンやメニューの外側の線がフォーカスにより下記のように太線になります。

フォーカスされた状態では太い線が表示
フォーカスされた状態では太い線が表示

フォーカス時に表示される太線はMenuButtonとMenuItemsのclassにoutline-noneを追加することで表示されなくなります。


<MenuButton
  class="
    px-4
    py-2
    border
    rounded-lg
    bg-blue-600
    hover:bg-blue-400
    text-white
    font-bold
    outline-none
  "
  >Menu</MenuButton
>
<MenuItems
  as="ul"
  class="w-[200px] divide-y border rounded-lg mt-2 outline-none"
>

アクセスビィリティの確認

マウスをメニューの上に乗せると背景色が変わりますがMenuボタンを押した後、キーボードの↑(上)、↓(下)キーを押すことでメニューを変更することができます。またメニューが開いた後にメニュー以外の要素をクリックするとメニューが閉じる以外にESCキー、Spaceキーを押してもメニューを閉じることができます。ボタンがフォーカスされている時にSpaceキーを押すとメニューの開閉を行うことができます。

メニューボタンのフォーカスはMenuButtonにoutline-noneを設定しない場合に表示される太線が表示される時にフォーカスされた状態であることがわかります。

Headless UIではこのような設定が行われいてることが”fully accessible UI components”という意味に関連しています。

アニメーションの設定

メニューを表示・非表示にアニメーションを設定したい場合はVueが持っているtransitionコンポーネントを利用します。下記のようにMenuItemsコンポーネントをtransitionコンポーネントで包むことでアニメーション設定を行うことでふわっとメニューが表示されるアニメーションを簡単に実装することができます。


<transition
  enter-active-class="transition duration-300 ease-out"
  enter-from-class="transform scale-95 opacity-0"
  enter-to-class="transform scale-100 opacity-100"
  leave-active-class="transition duration-300 ease-out"
  leave-from-class="transform scale-100 opacity-100"
  leave-to-class="transform scale-95 opacity-0"
>
  <MenuItems
    as="ul"
    class="w-[200px] divide-y border rounded-lg mt-2 outline-none"
  >
  //略
  </MenuItems>
</transition>

まとめ

ReactかVueの設定のどちらかを読み進めて”Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.”の意味を理解することができましたか?

一度Headless UIを設定を行うことで上記の文章を理解することができれば実装を考える必要なくスタイルを自由にカスタイズすることができるのでどこかでプロジェクトで利用してみようかなという気になってもらえたのではないでしょうか。他のコンポーネントについても機能は実装できてもスタイルが設定されていないので最初は少し戸惑うかもしれませんが手を動かしながらスタイルを設定していくことで使いこなすことができると思うので他の機能もぜひチャレンジしてみてください。