ReactやVue.jsでアプリケーションを開発する際、ドロップダウンメニューやダイアログ(モーダル)を実装したいという時に役に立つのがHeadless UIです。

Headless UIのドキュメントの概要説明Completely unstyled, fully accessible UI components, designed to integrate beautifully with Tailwind CSS.”を読んですぐにどのようなものか理解できる人は少ないと思いますが一度設定を行ってしまえば概要説明も理解できるはずです。ぜひ本文書でHeadless UIがどのようなものかしっかり理解してください。

Headless UIで利用できる機能(ドロップダウンメニューやダイアログなど)は事前に用意されているコンポーネントタグを利用するだけでそれらの機能を実装することが可能です。しかしそれらの機能にはスタイルが全く適用されていないのでTailwind CSSを利用して自分で設定を行う必要があります。機能はHeadless UIに任せることでデザインに注力することができ、オリジナルデザインのものを作成することができます。また自由にスタイルを設定を行えるだけではなくユーザのアクセスビリティも考慮に入れ実装されているので開いたドロップダウンメニューを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が開発しています。
fukidashi

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

React, VueどちらのプロジェクトもViteを利用して作成を行います。

Reactの場合

Viteでプロジェクトを作成するためにnpm create vite@latestコマンドを実行します。コマンドを実行すると対話的にプロジェクト名やフレームワーク、TypeScriptを利用するかどうかを設定する必要があります。ここではプロジェクト名に”headless-ui-react”, フレームワークに”React”, Variantに”JavaScript”を選択しています。


% npm create vite@latest

> npx
> create-vite

✔ Project name: … headless-ui-react
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/headless-ui-react...

Done. Now run:

  cd headless-ui-react
  npm install
  npm run dev

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


% npm run dev

> headless-ui-react@0.0.0 dev
> vite


  VITE v5.3.4  ready in 764 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

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

Vite+Reactのトップページ
Vite+Reactのトップページ

Vueの場合

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


% npm create vite@latest

> npx
> create-vite

✔ Project name: … headless-ui-vue
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript

Scaffolding project in /Users/mac/Desktop/headless-ui-vue...

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

> headless-ui-vue@0.0.0 dev
> vite


  VITE v5.3.4  ready in 760 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

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

Vue+Vite初期画面
Vue+Vite初期画面

TailwindCSSのインストール

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

Reactの場合


 % npm install -D tailwindcss postcss autoprefixer

設定ファイルを作成するためにinitコマンドを実行します。実行するとtailwind.config.js, postcss.config.jsファイルが作成されます。


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

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


/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,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コマンドを実行します。コマンドを実行するとtailwind.config.js、postcss.config.jsファイルが作成されます。


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

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


/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
};

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


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

インストール、設定が正常に完了しているか確認するために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@latest

ライブラリをインストール後にpackage.jsonファイルを確認してインストールしたライブラリとバージョンを確認しておきます。


{
  "name": "headless-ui-react",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@headlessui/react": "^2.1.2",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "autoprefixer": "^10.4.19",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.3",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-react-refresh": "^0.4.7",
    "postcss": "^8.4.39",
    "tailwindcss": "^3.4.6",
    "vite": "^5.3.4"
  }
}

Vueの場合


 % npm install @headlessui/vue@latest

ライブラリをインストール後にpackage.jsonファイルを確認してインストールしたライブラリとバージョンを確認しておきます。


{
  "name": "headless-ui-vue",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@headlessui/vue": "^1.7.22",
    "vue": "^3.4.31"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.39",
    "tailwindcss": "^3.4.6",
    "vite": "^5.3.4"
  }
}

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

インストールが完了したらHeadless UIを利用してドロップダウンメニューを実装していきます。最初に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, MenuButton, MenuItem, MenuItems } from '@headlessui/react';

function App() {
  return (
    <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>
  );
}

export default App;

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

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

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

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

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

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

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

propsの設定

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

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


//略
<Menu>
  <MenuButton>Menu</MenuButton>
  <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>
</Menu>
//略

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

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

スタイルの設定

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

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


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

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


import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';

function App() {
  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">ドロップダウンメニュー</h1>
      <Menu>
        <MenuButton className="px-4 py-2 border rounded-lg bg-blue-600 hover:bg-blue-400 text-white font-bold">
          Menu
        </MenuButton>
        <MenuItems
          as="ul"
          className="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>
  );
}

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

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


 % npm install @heroicons/react

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


import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { HomeIcon } from '@heroicons/react/24/outline';
//略

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

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


import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import {
  HomeIcon,
  Cog6ToothIcon,
  PhoneIcon,
  UsersIcon,
} from '@heroicons/react/24/outline';

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

export default App;

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

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

focusされている要素へのスタイル設定

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

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


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

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


import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import {
  HomeIcon,
  Cog6ToothIcon,
  PhoneIcon,
  UsersIcon,
} from '@heroicons/react/24/outline';

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

export default App;

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

activeになったメニュー
focusされたメニュー

ボタンがフォーカスされている時にスペースキーを押すとメニューが開閉でき、開いている時にはMenuボタンやメニューの外側の線がフォーカスにより下記のように太線になります。

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

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


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

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

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

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

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

アニメーションの設定

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


import {
  Menu,
  MenuButton,
  MenuItem,
  MenuItems,
  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"
>
  <MenuItems
    as="ul"
    className="w-[200px] divide-y border rounded-lg mt-2 outline-none"
  >
  //略
  </MenuItems>
</Transition>

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

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

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

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


<template>
  <div class="m-8">
    <h1 class="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 class="m-8">
    <h1 class="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 class="m-8">
    <h1 class="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">Home</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/service" class="p-2 inline-block">Service</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/aboutus" class="p-2 inline-block">About us</a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/contact" class="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,
  Cog6ToothIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/vue/24/outline';
</script>
<template>
  <div class="m-8">
    <h1 class="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 flex items-center">
            <HomeIcon class="w-5 y-5 mr-2" />
            <span>Home</span></a
          >
        </MenuItem>
        <MenuItem as="li">
          <a href="/service" class="p-2 flex items-center">
            <Cog6ToothIcon class="w-5 y-5 mr-2" />
            <span>Service</span>
          </a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/aboutus" class="p-2 flex items-center">
            <UsersIcon class="w-5 y-5 mr-2" />
            <span>About us</span>
          </a>
        </MenuItem>
        <MenuItem as="li">
          <a href="/contact" class="p-2 flex items-center">
            <PhoneIcon class="w-5 y-5 mr-2" />
            <span>Contact</span>
          </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 flex items-center rounded-t-lg"
  >
    <HomeIcon class="w-5 y-5 mr-2" />
    <span>Home</span></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,
  Cog6ToothIcon,
  UsersIcon,
  PhoneIcon,
} from '@heroicons/vue/24/outline';
</script>
<template>
  <div class="m-8">
    <h1 class="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 flex items-center rounded-t-lg"
          >
            <HomeIcon class="w-5 y-5 mr-2" />
            <span>Home</span></a
          >
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/service"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 flex items-center"
          >
            <Cog6ToothIcon class="w-5 y-5 mr-2" />
            <span>Service</span>
          </a>
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/aboutus"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 flex items-center"
          >
            <UsersIcon class="w-5 y-5 mr-2" />
            <span>About us</span>
          </a>
        </MenuItem>
        <MenuItem as="li" v-slot="{ active }">
          <a
            href="/contact"
            :class="active ? 'bg-violet-500 text-white' : 'text-gray-900'"
            class="p-2 flex items-center rounded-b-lg"
          >
            <PhoneIcon class="w-5 y-5 mr-2" />
            <span>Contact</span>
          </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を設定しない場合に表示される太線が表示される時にフォーカスされた状態であることがわかります。
fukidashi

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