メニュー (ドロップダウン)
メニューは、キーボードナビゲーションを強力にサポートする、カスタムのアクセシブルなドロップダウンコンポーネントを簡単に構築する方法を提供します。
はじめに、npmを使用してHeadless UIをインストールします。
npm install @headlessui/react
メニューボタンは、Menu
、Menu.Button
、Menu.Items
、およびMenu.Item
コンポーネントを使用して構築されます。
Menu.Button
をクリックすると、Menu.Items
が自動的に開閉します。メニューが開いているときは、項目のリストにフォーカスが当たり、キーボードで自動的にナビゲートできます。
import { Menu } from '@headlessui/react' function MyDropdown() { return ( <Menu> <Menu.Button>More</Menu.Button> <Menu.Items> <Menu.Item> {({ active }) => ( <a className={`${active && 'bg-blue-500'}`} href="/account-settings" > Account settings </a> )} </Menu.Item> <Menu.Item> {({ active }) => ( <a className={`${active && 'bg-blue-500'}`} href="/account-settings" > Documentation </a> )} </Menu.Item> <Menu.Item disabled> <span className="opacity-75">Invite a friend (coming soon!)</span> </Menu.Item> </Menu.Items> </Menu> ) }
Headless UIは、現在選択されているリストボックスオプション、ポップオーバーが開いているか閉じているか、メニュー内のどの項目が現在キーボードでアクティブになっているかなど、各コンポーネントに関する多くの状態を追跡します。
ただし、コンポーネントはヘッドレスであり、初期状態では完全にスタイルが適用されていないため、各状態に独自のスタイルを提供するまで、UIでこの情報を*視覚的に確認*することはできません。
各コンポーネントは、レンダープロップスを介して現在の状態に関する情報を公開します。これを使用して、条件付きで異なるスタイルを適用したり、異なるコンテンツをレンダリングしたりできます。
たとえば、Menu.Item
コンポーネントは、項目が現在マウスまたはキーボードでフォーカスされているかどうかを示すactive
状態を公開します。
import { Fragment } from 'react' import { Menu } from '@headlessui/react' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] function MyMenu() { return ( <Menu> <Menu.Button>Options</Menu.Button> <Menu.Items> {links.map((link) => ( /* Use the `active` state to conditionally style the active item. */ <Menu.Item key={link.href} as={Fragment}>
{({ active }) => (<a href={link.href} className={`${active ? 'bg-blue-500 text-white' : 'bg-white text-black'}`} > {link.label} </a> )} </Menu.Item> ))} </Menu.Items> </Menu> ) }
各コンポーネントの完全なレンダープロップスAPIについては、コンポーネントAPIドキュメントを参照してください。
各コンポーネントは、data-headlessui-state
属性を介して現在の状態に関する情報を公開します。これを使用して、条件付きで異なるスタイルを適用できます。
レンダープロップスAPIのいずれかの状態がtrue
の場合、それらはスペース区切りの文字列としてこの属性にリストされるため、[attr~=value]
形式のCSS属性セレクターを使用してターゲットにすることができます。
たとえば、メニューが開いていて2番目の項目がactive
になっているときに、子Menu.Item
コンポーネントを持つMenu.Items
コンポーネントがレンダリングされる方法は次のとおりです。
<!-- Rendered `Menu.Items` --> <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:*
などの修飾子でこの属性をターゲットにすることができます。
import { Menu } from '@headlessui/react' const links = [ { href: '/account-settings', label: 'Account settings' }, { href: '/support', label: 'Support' }, { href: '/license', label: 'License' }, { href: '/sign-out', label: 'Sign out' }, ] function MyMenu() { return ( <Menu> <Menu.Button>Options</Menu.Button> <Menu.Items> {links.map((link) => ( <Menu.Item as="a" key={link.href} href={link.href}
className="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"> {link.label} </Menu.Item> ))} </Menu.Items> </Menu> ) }
デフォルトでは、Menu.Items
インスタンスは、Menu
コンポーネント自体内で追跡される内部open
状態に基づいて自動的に表示/非表示されます。
import { Menu } from '@headlessui/react' function MyDropdown() { return ( <Menu> <Menu.Button>More</Menu.Button> {/* By default, the `Menu.Items` will automatically show/hide when the `Menu.Button` is pressed. */} <Menu.Items> <Menu.Item>{/* ... */}</Menu.Item> {/* ... */} </Menu.Items> </Menu> ) }
これを自分で処理したい場合(おそらく、何らかの理由で追加のラッパー要素を追加する必要があるため)、Menu.Items
インスタンスにstatic
プロップを追加して、常にレンダリングするように指示し、Menu
によって提供されるopen
スロットプロップを調べて、どの要素が表示/非表示になるかを自分で制御できます。
import { Menu } from '@headlessui/react' function MyDropdown() { return ( <Menu>
{({ open }) => (<> <Menu.Button>More</Menu.Button>{open && (<div> {/* Using the `static` prop, the `Menu.Items` are always rendered and the `open` state is ignored. */}<Menu.Items static><Menu.Item>{/* ... */}</Menu.Item> {/* ... */} </Menu.Items> </div> )} </> )} </Menu> ) }
メニューはデフォルトで閉じますが、サードパーティのLink
コンポーネントがevent.preventDefault()
を使用している場合があり、デフォルトの動作が妨げられるため、メニューが閉じません。
Menu
とMenu.Item
は、メニューを強制的に閉じるために使用できるclose()
レンダープロップスを公開します。
import { Menu } from '@headlessui/react' import { MyCustomLink } from './MyCustomLink' function MyMenu() { return ( <Menu> <Menu.Button>Terms</Menu.Button> <Menu.Items> <Menu.Item>
{({ close }) => (<MyCustomLink href="/" onClick={close}>Read and accept </MyCustomLink> )} </Menu.Item> </Menu.Items> </Menu> ) }
disabled
プロップを使用して、Menu.Item
を無効にします。これにより、キーボードナビゲーションで選択できなくなり、上下矢印キーを押してもスキップされます。
import { Menu } from '@headlessui/react' function MyDropdown() { return ( <Menu> <Menu.Button>More</Menu.Button> <Menu.Items> {/* ... */} {/* This item will be skipped by keyboard navigation. */}
<Menu.Item disabled><span className="opacity-75">Invite a friend (coming soon!)</span> </Menu.Item> {/* ... */} </Menu.Items> </Menu> ) }
メニューパネルの開閉をアニメーション化するには、提供されているTransition
コンポーネントを使用します。 Menu.Items
を<Transition>
でラップするだけで、トランジションが自動的に適用されます。
import { Menu, Transition } from '@headlessui/react' function MyDropdown() { return ( <Menu> <Menu.Button>More</Menu.Button> {/* Use the `Transition` component. */}
<Transitionenter="transition duration-100 ease-out"enterFrom="transform scale-95 opacity-0"enterTo="transform scale-100 opacity-100"leave="transition duration-75 ease-out"leaveFrom="transform scale-100 opacity-100"leaveTo="transform scale-95 opacity-0"><Menu.Items> <Menu.Item>{/* ... */}</Menu.Item> {/* ... */} </Menu.Items></Transition></Menu> ) }
デフォルトでは、組み込みのTransition
コンポーネントはMenu
コンポーネントと自動的に通信して、開閉状態を処理します。ただし、この動作をより詳細に制御する必要がある場合は、明示的に制御できます。
import { Menu, Transition } from '@headlessui/react' function MyDropdown() { return ( <Menu>
{({ open }) => (<><Menu.Button>More</Menu.Button> {/* Use the `Transition` component. */} <Transitionshow={open}enter="transition duration-100 ease-out" enterFrom="transform scale-95 opacity-0" enterTo="transform scale-100 opacity-100" leave="transition duration-75 ease-out" leaveFrom="transform scale-100 opacity-100" leaveTo="transform scale-95 opacity-0" > {/* Mark this component as `static` */}<Menu.Items static><Menu.Item>{/* ... */}</Menu.Item> {/* ... */} </Menu.Items> </Transition></>)}</Menu> ) }
レンダーレスであるため、Headless UIコンポーネントは、Framer MotionやReact Springなど、Reactエコシステムの他のアニメーションライブラリとも適切に連携します。
role="menu"
のアクセシビリティセマンティクスはかなり厳格であり、Menu.Item
コンポーネントではないMenu
の子は、スクリーンリーダーユーザーの期待どおりにメニューが機能するように、支援技術から自動的に非表示になります。
このため、Menu.Item
コンポーネント以外の子をレンダリングすることはお勧めしません。そのコンテンツは、支援技術を使用しているユーザーがアクセスできなくなるためです。
より柔軟なコンテンツのドロップダウンを作成する場合は、代わりにPopoverを使用することを検討してください。
デフォルトでは、Menu
とそのサブコンポーネントはそれぞれ、そのコンポーネントにとって適切なデフォルト要素をレンダリングします。
たとえば、Menu.Button
はデフォルトでbutton
をレンダリングし、Menu.Items
はdiv
をレンダリングします。対照的に、Menu
とMenu.Item
は*要素をレンダリングせず*、代わりにデフォルトで子を直接レンダリングします。
as
プロップを使用して、コンポーネントを異なる要素として、または独自のカスタムコンポーネントとしてレンダリングします。カスタムコンポーネントがrefを転送することを確認して、Headless UIが正しく機能するようにします。
import { forwardRef } from 'react' import { Menu } from '@headlessui/react'
let MyCustomButton = forwardRef(function (props, ref) {return <button className="..." ref={ref} {...props} />}) function MyDropdown() { return (<Menu><Menu.Button as={MyCustomButton}>More</Menu.Button><Menu.Items as="section"> <Menu.Item> {({ active }) => ( <a className={`${active && 'bg-blue-500'}`} href="/account-settings" > Account settings </a> )} </Menu.Item> {/* ... */} </Menu.Items> </Menu> ) }
ラッパー要素なしで子を直接レンダリングするように要素に指示するには、as={React.Fragment}
を使用します。
import { Menu } from '@headlessui/react' function MyDropdown() { return ( <Menu> {/* Render no wrapper, instead pass in a `button` manually. */}
<Menu.Button as={React.Fragment}><button>More</button> </Menu.Button> <Menu.Items> <Menu.Item> {({ active }) => ( <a className={`${active && 'bg-blue-500'}`} href="/account-settings" > Account settings </a> )} </Menu.Item> {/* ... */} </Menu.Items> </Menu> ) }
これは、Menu.Item
内で<a>
タグのようなインタラクティブな要素を使用している場合に重要です。 Menu.Item
にas="div"
がある場合、Headless UIによって提供されるプロップスはa
ではなくdiv
に転送されます。つまり、キーボードで<a>
タグによって提供されるURLにアクセスできなくなります。
Next.js v13より前は、Link
コンポーネントは不明なプロップスを基になるa
要素に転送せず、Menu.Item
内で使用されたときにクリックでメニューが閉じないようにしていました。
Next.js v12以前を使用している場合は、Link
をラップし、不明なプロップスを子のa
要素に転送する独自のコンポーネントを作成することで、この問題を回避できます。
import { forwardRef } from 'react'import Link from 'next/link'import { Menu } from '@headlessui/react'const MyLink = forwardRef((props, ref) => {let { href, children, ...rest } = propsreturn (<Link href={href}><a ref={ref} {...rest}>{children}</a></Link>)})function Example() { return ( <Menu> <Menu.Button>More</Menu.Button> <Menu.Items> <Menu.Item><MyLink href="/profile">Profile</MyLink></Menu.Item> </Menu.Items> </Menu> ) }
これにより、Headless UIがa
要素に追加する必要があるすべてのイベントリスナーが正しく適用されます。
この動作はNext.js v13で変更され、この回避策は不要になりました。
Menu.Button
をクリックすると、メニューが切り替わり、Menu.Items
コンポーネントにフォーカスが当たります。フォーカスは、Escapeキーが押されるか、ユーザーがメニューの外側をクリックするまで、開いているメニュー内にトラップされます。メニューを閉じると、フォーカスはMenu.Button
に戻ります。
Menu.Button
をクリックすると、メニューが切り替わります。開いているメニューの外側をクリックすると、そのメニューが閉じます。
コマンド | 説明 |
| メニューを開き、無効になっていない最初の項目にフォーカスを当てる |
下矢印または上矢印 | メニューを開き、無効化されていない最初の/最後の項目にフォーカスします |
メニューが開いているときに Esc | 開いているすべてのメニューを閉じます |
下矢印または上矢印メニューが開いている場合 | 無効化されていない前/次の項目にフォーカスします |
メニューが開いているときに HomeまたはPageUp | 無効化されていない最初の項目にフォーカスします |
メニューが開いているときに EndまたはPageDown | 無効化されていない最後の項目にフォーカスします |
メニューが開いているときに EnterまたはSpace | 現在のメニュー項目をアクティブ化/クリックします |
メニューが開いているときに A–Zまたはa–z | キーボード入力に一致する最初の項目にフォーカスします |
関連するすべての ARIA 属性は自動的に管理されます。
Menu
に実装されているすべてのアクセシビリティ機能の完全なリファレンスについては、メニューボタンに関する ARIA 仕様を参照してください。
メニューは、ほとんどのオペレーティングシステムのタイトルバーにあるメニューのような UI 要素に最適です。メニューには特定のアクセシビリティセマンティクスがあり、その内容はリンクまたはボタンのリストに限定する必要があります。フォーカスは開いているメニューにトラップされるため、コンテンツをタブ移動したり、メニューから離れたりすることはできません。代わりに、矢印キーを使用してメニューの項目間を移動します。
Headless UI の他の同様のコンポーネントを使用する場合の例を次に示します
-
<Popover />
。ポップオーバーは汎用のフローティングメニューです。これらは、トリガーとなるボタンの近くに表示され、画像やクリックできないコンテンツなど、任意のマークアップを配置できます。Tab キーは、他の通常のマークアップと同様に、ポップオーバーのコンテンツ内を移動します。拡張可能なコンテンツとフライアウトパネルを備えたヘッダーナビゲーション項目を作成するのに最適です。 -
<Disclosure />
。ディスクロージャーは、切り替え可能な FAQ セクションなど、追加情報を表示するために展開する要素に役立ちます。通常、インラインでレンダリングされ、表示または非表示にするとドキュメントがリフローされます。 -
<Dialog />
。ダイアログは、ユーザーの注意を完全に引くことを目的としています。通常、画面の中央にフローティングパネルをレンダリングし、バックドロップを使用してアプリケーションの残りのコンテンツを暗くします。また、フォーカスを取得し、ダイアログが閉じられるまで、ダイアログのコンテンツからタブ移動できないようにします。
プロパティ | デフォルト | 説明 |
as | Fragment | 文字列 | コンポーネント
|
レンダープロップ | 説明 |
open |
メニューが開いているかどうか。 |
close |
メニューを閉じて、 |
プロパティ | デフォルト | 説明 |
as | button | 文字列 | コンポーネント
|
レンダープロップ | 説明 |
open |
メニューが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | div | 文字列 | コンポーネント
|
static | false | ブール値 要素が内部的に管理される開閉状態を無視するかどうか。 注: |
unmount | true | ブール値 開閉状態に基づいて、要素をアンマウントするか非表示にするか。 注: |
レンダープロップ | 説明 |
open |
メニューが開いているかどうか。 |
プロパティ | デフォルト | 説明 |
as | Fragment | 文字列 | コンポーネント
|
disabled | false | ブール値 キーボードナビゲーションと ARIA の目的でアイテムを無効にするかどうか。 |
レンダープロップ | 説明 |
active |
アイテムがリスト内のアクティブ/フォーカスされているアイテムかどうか。 |
disabled |
キーボードナビゲーションと ARIA の目的でアイテムが無効になっているかどうか |
close |
メニューを閉じて、 |