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

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

はじめに、npmを使用してHeadless UIをインストールします。

npm install @headlessui/react

メニューボタンは、MenuMenu.ButtonMenu.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()を使用している場合があり、デフォルトの動作が妨げられるため、メニューが閉じません。

MenuMenu.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. */}
<Transition
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"
>
<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. */} <Transition
show={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 MotionReact Springなど、Reactエコシステムの他のアニメーションライブラリとも適切に連携します。

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

このため、Menu.Itemコンポーネント以外の子をレンダリングすることはお勧めしません。そのコンテンツは、支援技術を使用しているユーザーがアクセスできなくなるためです。

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

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

たとえば、Menu.Buttonはデフォルトでbuttonをレンダリングし、Menu.Itemsdivをレンダリングします。対照的に、MenuMenu.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.Itemas="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 } = props
return (
<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をクリックすると、メニューが切り替わります。開いているメニューの外側をクリックすると、そのメニューが閉じます。

コマンド説明

Menu.ButtonにフォーカスがあるときにEnterまたはSpaceキーを押す

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

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

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

メニューが開いているときに Esc

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

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

無効化されていない前/次の項目にフォーカスします

メニューが開いているときに HomeまたはPageUp

無効化されていない最初の項目にフォーカスします

メニューが開いているときに EndまたはPageDown

無効化されていない最後の項目にフォーカスします

メニューが開いているときに EnterまたはSpace

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

メニューが開いているときに A–Zまたはa–z

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

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

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

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

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

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

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

  • <Dialog />。ダイアログは、ユーザーの注意を完全に引くことを目的としています。通常、画面の中央にフローティングパネルをレンダリングし、バックドロップを使用してアプリケーションの残りのコンテンツを暗くします。また、フォーカスを取得し、ダイアログが閉じられるまで、ダイアログのコンテンツからタブ移動できないようにします。

プロパティデフォルト説明
asFragment
文字列 | コンポーネント

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

レンダープロップ説明
open

ブール値

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

close

() => void

メニューを閉じて、Menu.Button にフォーカスを戻します。

プロパティデフォルト説明
asbutton
文字列 | コンポーネント

Menu.Button がレンダリングされる要素またはコンポーネント。

レンダープロップ説明
open

ブール値

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

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

Menu.Items がレンダリングされる要素またはコンポーネント。

staticfalse
ブール値

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

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

unmounttrue
ブール値

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

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

レンダープロップ説明
open

ブール値

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

プロパティデフォルト説明
asFragment
文字列 | コンポーネント

Menu.Item がレンダリングされる要素またはコンポーネント。

disabledfalse
ブール値

キーボードナビゲーションと ARIA の目的でアイテムを無効にするかどうか。

レンダープロップ説明
active

ブール値

アイテムがリスト内のアクティブ/フォーカスされているアイテムかどうか。

disabled

ブール値

キーボードナビゲーションと ARIA の目的でアイテムが無効になっているかどうか

close

() => void

メニューを閉じて、Menu.Button にフォーカスを戻します。

Headless UI と Tailwind CSS を使用した、事前にデザインされたコンポーネントの例に興味がある場合は、Tailwind UI をご覧ください。これは、私たちが作成した美しくデザインされ、巧みに作られたコンポーネントのコレクションです。

これは、このようなオープンソースプロジェクトでの私たちの仕事をサポートするための素晴らしい方法であり、私たちがそれらを改善し、適切に維持することを可能にします。