メニュー (ドロップダウン)

メニューは、キーボードナビゲーションを強力にサポートした、カスタムでアクセシブルなドロップダウンコンポーネントを簡単に構築する方法を提供します。

まず、npm経由でHeadless UIをインストールします。

このライブラリはVue 3のみをサポートしていることに注意してください。

npm install @headlessui/vue

メニューボタンは、MenuMenuButtonMenuItems、およびMenuItemコンポーネントを使用して構築されます。

MenuButtonはクリックされると自動的にMenuItemsを開閉し、メニューが開いているときは、項目のリストがフォーカスを受け、キーボードで自動的にナビゲートできます。

<template> <Menu> <MenuButton>More</MenuButton> <MenuItems> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Documentation </a> </MenuItem> <MenuItem disabled> <span class="opacity-75">Invite a friend (coming soon!)</span> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

Headless UIは、どのリストボックスオプションが現在選択されているか、ポップオーバーが開いているか閉じているか、キーボードで現在どのメニュー項目がアクティブになっているかなど、各コンポーネントに関する多くの状態を追跡します。

しかし、コンポーネントはヘッドレスであり、デフォルトでは完全にスタイルが適用されていないため、各状態に必要なスタイルを自分で提供するまで、UIでこの情報を確認することはできません。

各コンポーネントは、スロットプロパティを介して現在の状態に関する情報を公開します。これを使用して、異なるスタイルを条件付きで適用したり、異なるコンテンツをレンダリングしたりできます。

たとえば、MenuItemコンポーネントはactive状態を公開します。これは、項目が現在マウスまたはキーボードでフォーカスされているかどうかを示します。

<template> <Menu> <MenuButton>Options</MenuButton> <MenuItems> <!-- Use the `active` state to conditionally style the active item. --> <MenuItem v-for="link in links" :key="link.href" as="template"
v-slot="{ active }"
>
<a :href="link.href"
:class="{ 'bg-blue-500 text-white': active, 'bg-white text-black': !active }"
>
{{ link.label }} </a> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] </script>

利用可能なすべてのスロットプロパティの完全なリストについては、コンポーネントAPIドキュメントを参照してください。

各コンポーネントは、data-headlessui-state属性を介して現在の状態に関する情報も公開します。これを使用して、異なるスタイルを条件付きで適用できます。

スロットプロパティAPIの状態がtrueの場合、それらはスペースで区切られた文字列としてこの属性にリストされるため、[attr~=value]形式のCSS属性セレクターでターゲットにできます。

たとえば、メニューが開いていて、2番目の項目がactiveの場合、子MenuItemコンポーネントを持つMenuItemsコンポーネントは次のようにレンダリングされます。

<!-- Rendered `MenuItems` --> <ul data-headlessui-state="open"> <li data-headlessui-state="">Account settings</li> <li data-headlessui-state="active">Support</li> <li data-headlessui-state="">License</li> </ul>

Tailwind CSSを使用している場合は、@headlessui/tailwindcssプラグインを使用して、ui-open:*ui-active:*などの修飾子でこの属性をターゲットにできます。

<template> <Menu> <MenuButton>Options</MenuButton> <MenuItems> <!-- Use the `active` state to conditionally style the active item. --> <MenuItem v-for="link in links" :key="link.href" :href="link.href" as="a"
class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>
{{ link.label }} </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] </script>

デフォルトでは、MenuItemsインスタンスは、Menuコンポーネント自体内で追跡される内部のopen状態に基づいて、自動的に表示/非表示になります。

<template> <Menu> <MenuButton>More</MenuButton> <!-- By default, the `MenuItems` will automatically show/hide when the `MenuButton` is pressed. --> <MenuItems> <MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

(何らかの理由で追加のラッパー要素を追加する必要があるなどの理由で)これを自分で処理したい場合は、MenuItemsインスタンスにstaticプロパティを追加して常にレンダリングするように指示し、Menuによって提供されるopenスロットプロパティを検査して、自分でどの要素を表示/非表示にするかを制御できます。

<template>
<Menu v-slot="{ open }">
<MenuButton>More</MenuButton>
<div v-show="open">
<!-- Using the `static` prop, the `MenuItems` are always rendered and the `open` state is ignored. -->
<MenuItems static>
<MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </div> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

メニューはデフォルトで既に閉じますが、サードパーティのLinkコンポーネントがevent.preventDefault()を使用し、デフォルトの動作を妨げ、したがってメニューが閉じないということが発生する可能性があります。

MenuMenuItemは、メニューを命令的に閉じるために使用できるclose()スロットプロパティを公開します。

<template> <Menu> <MenuButton>Terms</MenuButton> <MenuItems>
<MenuItem v-slot="{ close }">
<MyCustomLink href="/" @click="close">Read and accept</MyCustomLink>
</MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' import { MyCustomLink } from './MyCustomLink' </script>

MenuItemを無効にするには、disabledプロパティを使用します。これにより、キーボードナビゲーションで選択できなくなり、上/下矢印を押すとスキップされます。

<template> <Menu> <MenuButton>More</MenuButton> <MenuItems> <!-- ... --> <!-- This item will be skipped by keyboard navigation. -->
<MenuItem disabled>
<span class="opacity-75">Invite a friend (coming soon!)</span> </MenuItem> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

メニューパネルの開閉をアニメーション化するには、Vueに組み込まれている<transition>コンポーネントを使用できます。MenuItemsインスタンスを<transition>でラップするだけで、トランジションが自動的に適用されます。

<template> <Menu> <MenuButton>More</MenuButton> <!-- Use Vue's built-in `transition` element to add transitions. -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<MenuItems> <MenuItem><!-- ... --></MenuItem> <!-- ... --> </MenuItems> </transition> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

メニューの異なる子の複数のトランジションを調整したい場合は、Headless UIに含まれているTransitionコンポーネントを確認してください。

role="menu"のアクセシビリティセマンティクスはかなり厳密であり、MenuItemコンポーネントではないMenuの子は、メニューがスクリーンリーダーユーザーが期待するように機能するように、支援技術から自動的に非表示になります。

このため、MenuItemコンポーネント以外のコンテンツをレンダリングすると、支援技術を使用している人にとってそのコンテンツにアクセスできなくなるため、推奨されません。

より柔軟なコンテンツを持つドロップダウンを構築する場合は、代わりにPopoverの使用を検討してください。

デフォルトでは、Menuとそのサブコンポーネントはそれぞれ、そのコンポーネントに適切なデフォルトの要素をレンダリングします。

たとえば、MenuButtonはデフォルトでbuttonをレンダリングし、MenuItemsdivをレンダリングします。対照的に、MenuMenuItem要素をレンダリングせず、代わりにデフォルトで子を直接レンダリングします。

これは、すべてのコンポーネントに存在するasプロパティを使用して簡単に変更できます。

<template> <!-- Render a `div` instead of no wrapper element -->
<Menu as="div">
<MenuButton>More</MenuButton> <!-- Render a `section` instead of a `div` -->
<MenuItems as="section">
<MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

ラッパー要素なしで子を直接レンダリングするように要素に指示するには、as="template"を使用します。

<template> <Menu> <!-- Render no wrapper, instead pass in a `button` manually. -->
<MenuButton as="template">
<button>More</button> </MenuButton> <MenuItems> <MenuItem v-slot="{ active }"> <a :class='{ "bg-blue-500": active }' href="/account-settings"> Account settings </a> </MenuItem> <!-- ... --> </MenuItems> </Menu> </template> <script setup> import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue' </script>

これは、MenuItem内に<a>タグなどのインタラクティブな要素を使用している場合に重要です。MenuItemas="div"がある場合、Headless UIによって提供されるプロパティはaではなくdivに転送されるため、キーボードから<a>タグによって提供されるURLに移動できなくなります。

MenuButtonをクリックすると、メニューが切り替わり、MenuItemsコンポーネントにフォーカスが移ります。フォーカスは、Escapeキーが押されるか、ユーザーがメニューの外側をクリックするまで、開いているメニュー内に保持されます。メニューを閉じると、フォーカスがMenuButtonに戻ります。

MenuButtonをクリックすると、メニューが切り替わります。開いているメニューの外側をクリックすると、そのメニューが閉じます。

コマンド説明

Enter または Space MenuButtonにフォーカスがある場合

メニューを開き、最初の無効になっていない項目にフォーカスを当てます

下矢印 または 上矢印MenuButtonにフォーカスがある場合

メニューを開き、最初/最後の無効になっていない項目にフォーカスを当てます

Esc メニューが開いている場合

開いているすべてのメニューを閉じます

下矢印 または 上矢印メニューが開いている場合

前/次の無効になっていない項目にフォーカスを当てます

Home または PageUp メニューが開いている場合

最初の無効になっていない項目にフォーカスを当てます

End または PageDown メニューが開いている場合

最後の無効になっていない項目にフォーカスを当てます

Enter または Space メニューが開いている場合

現在のメニュー項目をアクティブ化/クリックします

A–Z または a–z メニューが開いている場合

キーボード入力に一致する最初の項目にフォーカスを当てます

関連するすべてのARIA属性は自動的に管理されます。

Menuに実装されているすべてのアクセシビリティ機能の完全なリファレンスについては、メニューボタンに関するARIA仕様を参照してください。

メニューは、ほとんどのオペレーティングシステムのタイトルバーにあるメニューのようなUI要素に最適です。これらには特定のアクセシビリティセマンティクスがあり、コンテンツはリンクまたはボタンのリストに制限する必要があります。フォーカスは開いているメニュー内に保持されるため、コンテンツをTabキーで移動したり、メニューから離れたりすることはできません。代わりに、矢印キーでメニューの項目をナビゲートします。

Headless UIの他の同様のコンポーネントを使用する場面を次に示します。

  • <Popover />。ポップオーバーは汎用的なフローティングメニューです。ポップオーバーは、それをトリガーするボタンの近くに表示され、画像やクリックできないコンテンツなど、任意のマークアップを配置できます。Tabキーは、他の通常のマークアップと同様に、ポップオーバーの内容をナビゲートします。拡張可能なコンテンツやフライアウトパネルを備えたヘッダーナビゲーション項目の構築に最適です。

  • <Disclosure />。ディスクロージャーは、トグル可能なFAQセクションのように、追加情報を表示するために展開する要素に役立ちます。通常はインラインでレンダリングされ、表示または非表示にするとドキュメントがリフローされます。

  • <Dialog />。ダイアログは、ユーザーの注意を完全に引き付けることを目的としています。通常、画面の中央にフローティングパネルをレンダリングし、アプリケーションの残りのコンテンツを暗くするために背景を使用します。また、ダイアログが閉じられるまで、フォーカスをキャプチャし、ダイアログの内容からTabキーで移動することを防ぎます。

プロパティデフォルト説明
astemplate
String | Component

Menuがレンダリングする要素またはコンポーネント。

スロットプロパティ説明
open

Boolean

メニューが開いているかどうか。

close

() => void

メニューを閉じ、MenuButtonにフォーカスを戻します。

プロパティデフォルト説明
asbutton
String | Component

MenuButtonがレンダリングする要素またはコンポーネント。

スロットプロパティ説明
open

Boolean

メニューが開いているかどうか。

プロパティデフォルト説明
asdiv
String | Component

MenuItemsがレンダリングする要素またはコンポーネント。

staticfalse
Boolean

要素が内部で管理される開閉状態を無視するかどうか。

注:staticunmountは同時に使用できません。そうするとTypeScriptエラーが発生します。

unmounttrue
Boolean

開閉状態に基づいて、要素をアンマウントするか非表示にするか。

注:staticunmountは同時に使用できません。そうするとTypeScriptエラーが発生します。

スロットプロパティ説明
open

Boolean

メニューが開いているかどうか。

プロパティデフォルト説明
astemplate
String | Component

MenuItemがレンダリングする要素またはコンポーネント。

disabledfalse
Boolean

キーボードナビゲーションおよびARIAの目的で、項目を無効にするかどうか。

スロットプロパティ説明
active

Boolean

項目がリスト内のアクティブ/フォーカスされた項目であるかどうか。

disabled

Boolean

キーボードナビゲーションおよびARIAの目的で、項目が無効になっているかどうか。

close

() => void

メニューを閉じ、MenuButtonにフォーカスを戻します。

Headless UIとTailwind CSSを使用した、事前設計されたコンポーネントの例に興味がある場合は、当社が作成した美しくデザインされ、専門的に作成されたコンポーネントのコレクションであるTailwind UIをご覧ください。

これは、このようなオープンソースプロジェクトでの私たちの活動を支援するのに最適な方法であり、それらを改善し、適切に維持できるようにします。