メニュー (ドロップダウン)
メニューは、キーボードナビゲーションを強力にサポートした、カスタムでアクセシブルなドロップダウンコンポーネントを簡単に構築する方法を提供します。
まず、npm経由でHeadless UIをインストールします。
このライブラリはVue 3のみをサポートしていることに注意してください。
npm install @headlessui/vue
メニューボタンは、Menu
、MenuButton
、MenuItems
、および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()
を使用し、デフォルトの動作を妨げ、したがってメニューが閉じないということが発生する可能性があります。
Menu
とMenuItem
は、メニューを命令的に閉じるために使用できる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. -->
<transitionenter-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
をレンダリングし、MenuItems
はdiv
をレンダリングします。対照的に、Menu
とMenuItem
は要素をレンダリングせず、代わりにデフォルトで子を直接レンダリングします。
これは、すべてのコンポーネントに存在する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>
タグなどのインタラクティブな要素を使用している場合に重要です。MenuItem
にas="div"
がある場合、Headless UIによって提供されるプロパティはa
ではなくdiv
に転送されるため、キーボードから<a>
タグによって提供されるURLに移動できなくなります。
MenuButton
をクリックすると、メニューが切り替わり、MenuItems
コンポーネントにフォーカスが移ります。フォーカスは、Escapeキーが押されるか、ユーザーがメニューの外側をクリックするまで、開いているメニュー内に保持されます。メニューを閉じると、フォーカスがMenuButton
に戻ります。
MenuButton
をクリックすると、メニューが切り替わります。開いているメニューの外側をクリックすると、そのメニューが閉じます。
コマンド | 説明 |
Enter または Space | メニューを開き、最初の無効になっていない項目にフォーカスを当てます |
下矢印 または 上矢印 | メニューを開き、最初/最後の無効になっていない項目にフォーカスを当てます |
Esc メニューが開いている場合 | 開いているすべてのメニューを閉じます |
下矢印 または 上矢印メニューが開いている場合 | 前/次の無効になっていない項目にフォーカスを当てます |
Home または PageUp メニューが開いている場合 | 最初の無効になっていない項目にフォーカスを当てます |
End または PageDown メニューが開いている場合 | 最後の無効になっていない項目にフォーカスを当てます |
Enter または Space メニューが開いている場合 | 現在のメニュー項目をアクティブ化/クリックします |
A–Z または a–z メニューが開いている場合 | キーボード入力に一致する最初の項目にフォーカスを当てます |
関連するすべてのARIA属性は自動的に管理されます。
Menu
に実装されているすべてのアクセシビリティ機能の完全なリファレンスについては、メニューボタンに関するARIA仕様を参照してください。
メニューは、ほとんどのオペレーティングシステムのタイトルバーにあるメニューのようなUI要素に最適です。これらには特定のアクセシビリティセマンティクスがあり、コンテンツはリンクまたはボタンのリストに制限する必要があります。フォーカスは開いているメニュー内に保持されるため、コンテンツをTabキーで移動したり、メニューから離れたりすることはできません。代わりに、矢印キーでメニューの項目をナビゲートします。
Headless UIの他の同様のコンポーネントを使用する場面を次に示します。
-
<Popover />
。ポップオーバーは汎用的なフローティングメニューです。ポップオーバーは、それをトリガーするボタンの近くに表示され、画像やクリックできないコンテンツなど、任意のマークアップを配置できます。Tabキーは、他の通常のマークアップと同様に、ポップオーバーの内容をナビゲートします。拡張可能なコンテンツやフライアウトパネルを備えたヘッダーナビゲーション項目の構築に最適です。 -
<Disclosure />
。ディスクロージャーは、トグル可能なFAQセクションのように、追加情報を表示するために展開する要素に役立ちます。通常はインラインでレンダリングされ、表示または非表示にするとドキュメントがリフローされます。 -
<Dialog />
。ダイアログは、ユーザーの注意を完全に引き付けることを目的としています。通常、画面の中央にフローティングパネルをレンダリングし、アプリケーションの残りのコンテンツを暗くするために背景を使用します。また、ダイアログが閉じられるまで、フォーカスをキャプチャし、ダイアログの内容からTabキーで移動することを防ぎます。
プロパティ | デフォルト | 説明 |
as | template | String | Component
|
スロットプロパティ | 説明 |
open |
メニューが開いているかどうか。 |
close |
メニューを閉じ、 |
プロパティ | デフォルト | 説明 |
as | button | String | Component
|
スロットプロパティ | 説明 |
open |
メニューが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | div | String | Component
|
static | false | Boolean 要素が内部で管理される開閉状態を無視するかどうか。 注: |
unmount | true | Boolean 開閉状態に基づいて、要素をアンマウントするか非表示にするか。 注: |
スロットプロパティ | 説明 |
open |
メニューが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | template | String | Component
|
disabled | false | Boolean キーボードナビゲーションおよびARIAの目的で、項目を無効にするかどうか。 |
スロットプロパティ | 説明 |
active |
項目がリスト内のアクティブ/フォーカスされた項目であるかどうか。 |
disabled |
キーボードナビゲーションおよびARIAの目的で、項目が無効になっているかどうか。 |
close |
メニューを閉じ、 |