ドロップダウンメニュー
メニューは、キーボードナビゲーションを強力にサポートした、カスタムでアクセシブルなドロップダウンコンポーネントを簡単に構築できる方法を提供します。
インストール
開始するには、npm を介して Headless UI をインストールします。
npm install @headlessui/react
基本例
メニューは、Menu
、MenuButton
、MenuItems
、およびMenuItem
コンポーネントを使用して構築されます。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
MenuButton
は、クリックされたときにMenuItems
を自動的に開閉し、メニューが開かれると、アイテムのリストがフォーカスを受け取り、キーボードで操作できるようになります。
スタイリング
Headless UI は、キーボードで現在フォーカスされているメニュー項目、ポップオーバーが開いているか閉じているか、現在選択されているリストボックスオプションなど、各コンポーネントに関する多くの状態を追跡します。
しかし、コンポーネントはヘッドレスで、すぐに使える状態では完全にスタイルが適用されていないため、自分で各状態のスタイルを提供するまで、UIでこの情報を見ることはできません。
データ属性の使用
Headless UI コンポーネントのさまざまな状態をスタイル設定する最も簡単な方法は、各コンポーネントが公開するdata-*
属性を使用することです。
たとえば、MenuButton
コンポーネントはdata-active
属性を公開しており、これはメニューが現在開いているかどうかを示し、MenuItem
コンポーネントはdata-focus
属性を公開しており、これはマウスまたはキーボードでメニュー項目が現在フォーカスされているかどうかを示します。
<!-- Rendered `MenuButton`, `MenuItems`, and `MenuItem` -->
<button data-active>Options</button>
<div data-open>
<a href="/settings">Settings</a>
<a href="/support" data-focus>Support</a>
<a href="/license">License</a>
</div>
これらのデータ属性の存在に基づいて条件付きでスタイルを適用するには、CSS 属性セレクターを使用します。Tailwind CSS を使用している場合、データ属性修飾子を使用すると簡単になります。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
const links = [
{ href: '/settings', label: 'Settings' },
{ href: '/support', label: 'Support' },
{ href: '/license', label: 'License' },
]
function Example() {
return (
<Menu>
<MenuButton className="data-[active]:bg-blue-200">My account</MenuButton> <MenuItems anchor="bottom">
{links.map((link) => (
<MenuItem key={link.href} className="block data-[focus]:bg-blue-100"> <a href={link.href}>{link.label}</a>
</MenuItem>
))}
</MenuItems>
</Menu>
)
}
利用可能なすべてのデータ属性の一覧については、コンポーネントAPIを参照してください。
レンダープロップスの使用
各コンポーネントは、レンダープロップスを介して現在の状態に関する情報を公開しており、これを使用して条件付きで異なるスタイルを適用したり、異なるコンテンツをレンダリングしたりできます。
たとえば、MenuButton
コンポーネントはactive
状態を公開しており、これはメニューが現在開いているかどうかを示し、MenuItem
コンポーネントはfocus
状態を公開しており、これはマウスまたはキーボードでメニュー項目が現在フォーカスされているかどうかを示します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import clsx from 'clsx'
import { Fragment } from 'react'
const links = [
{ href: '/settings', label: 'Settings' },
{ href: '/support', label: 'Support' },
{ href: '/license', label: 'License' },
]
function Example() {
return (
<Menu>
<MenuButton as={Fragment}> {({ active }) => <button className={clsx(active && 'bg-blue-200')}>My account</button>} </MenuButton> <MenuItems anchor="bottom">
{links.map((link) => (
<MenuItem key={link.href} as={Fragment}> {({ focus }) => ( <a className={clsx('block', focus && 'bg-blue-100')} href={link.href}> {link.label} </a> )} </MenuItem> ))}
</MenuItems>
</Menu>
)
}
利用可能なすべてのレンダープロップスのリストについては、コンポーネントAPIを参照してください。
例
ボタンとの使用
リンクに加えて、MenuItem
でボタンを使用することもできます。これは、ダイアログを開いたり、フォームを送信したりするなどのアクションをトリガーする場合に便利です。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
function showSettingsDialog() {
alert('Open settings dialog!')
}
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem> <button onClick={showSettingsDialog} className="block w-full text-left data-[focus]:bg-blue-100"> Settings </button> </MenuItem> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
<form action="/logout" method="post"> <MenuItem> <button type="submit" className="block w-full text-left data-[focus]:bg-blue-100"> Sign out </button> </MenuItem> </form> </MenuItems>
</Menu>
)
}
アイテムの無効化
disabled
プロップを使用してMenuItem
を無効にし、選択できないようにします。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
<MenuItem disabled> <a className="block data-[disabled]:opacity-50" href="/invite-a-friend"> Invite a friend (coming soon!) </a> </MenuItem> </MenuItems>
</Menu>
)
}
アイテムの区切り
MenuSeparator
コンポーネントを使用して、メニュー内のアイテム間に視覚的な区切りを追加します。
import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuSeparator className="my-1 h-px bg-black" /> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
アイテムのグループ化
MenuSection
、MenuHeading
、およびMenuSeparator
コンポーネントを使用して、ラベル付きのセクションにアイテムをグループ化します。
import { Menu, MenuButton, MenuHeading, MenuItem, MenuItems, MenuSection, MenuSeparator } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuSection> <MenuHeading className="text-sm opacity-50">Settings</MenuHeading> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/profile">
My profile
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/notifications">
Notifications
</a>
</MenuItem>
</MenuSection> <MenuSeparator className="my-1 h-px bg-black" /> <MenuSection> <MenuHeading className="text-sm opacity-50">Support</MenuHeading> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Documentation
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuSection> </MenuItems>
</Menu>
)
}
ドロップダウン幅の設定
MenuItems
ドロップダウンには、デフォルトでは幅が設定されていませんが、CSSを使用して追加できます。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom" className="w-52"> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
ドロップダウンの幅をMenuButton
の幅と一致させたい場合は、MenuItems
要素で公開されている--button-width
CSS 変数を使用します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom" className="w-[var(--button-width)]"> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
ドロップダウンの位置決め
MenuItems
にanchor
プロップを追加して、MenuButton
を基準にドロップダウンを自動的に配置します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom start"> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
top
、right
、bottom
、またはleft
の値を使用して、適切なエッジに沿ってドロップダウンを中央揃えするか、start
またはend
と組み合わせて、top start
またはbottom end
など、特定のコーナーにドロップダウンを配置します。
ボタンとドロップダウンの間の間隔を制御するには、--anchor-gap
CSS 変数を使用します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom start" className="[--anchor-gap:4px] sm:[--anchor-gap:8px]"> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
さらに、--anchor-offset
を使用してドロップダウンを元の位置から移動する距離を制御し、--anchor-padding
を使用してドロップダウンとビューポートの間にあるべき最小スペースを制御できます。
anchor
プロップは、JavaScriptを使用してgap
、offset
、およびpadding
の値を制御できるオブジェクトAPIもサポートしています。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems anchor={{ to: 'bottom start', gap: '4px' }}> <MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
これらのオプションの詳細については、MenuItems APIを参照してください。
トランジションの追加
ドロップダウンの開閉をアニメーション化するには、MenuItems
コンポーネントにtransition
プロップを追加し、CSSを使用してトランジションのさまざまな段階をスタイル設定します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
function Example() {
return (
<Menu>
<MenuButton>My account</MenuButton>
<MenuItems
anchor="bottom"
transition className="origin-top transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0" >
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
内部的には、transition
プロップはTransition
コンポーネントとまったく同じ方法で実装されています。詳細については、トランジションドキュメントを参照してください。
Framer Motion を使用したアニメーション
Headless UI は、Framer MotionやReact SpringなどのReactエコシステムの他のアニメーションライブラリともよく連携します。これらのライブラリにいくつかの状態を公開するだけです。
たとえば、Framer Motion でメニューをアニメーション化するには、MenuItems
コンポーネントにstatic
プロップを追加し、open
レンダープロップに基づいて条件付きでレンダリングします。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
function Example() {
return (
<Menu>
{({ open }) => ( <>
<MenuButton>My account</MenuButton>
<AnimatePresence>
{open && ( <MenuItems
static as={motion.div}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
anchor="bottom"
className="origin-top"
>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
)} </AnimatePresence>
</>
)} </Menu>
)
}
メニューの手動クローズ
デフォルトでは、MenuItem
をクリックするとMenu
が閉じます。ただし、一部のサードパーティのLink
コンポーネントはevent.preventDefault()
を使用しており、メニューが閉じることができません。
このような状況では、Menu
コンポーネントとMenuItem
コンポーネントの両方で使用可能なclose
レンダープロップを使用して、メニューを強制的に閉じることができます。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { MyCustomLink } from './MyCustomLink'
function Example() {
return (
<Menu>
<MenuButton>Terms</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
{({ close }) => ( <MyCustomLink href="/" onClick={close}> Read and accept
</MyCustomLink>
)} </MenuItem>
</MenuItems>
</Menu>
)
}
デフォルトでは、Menu
とそのサブコンポーネントはそれぞれ、そのコンポーネントに適したデフォルトの要素をレンダリングします。
たとえば、MenuButton
はデフォルトでbutton
をレンダリングし、MenuItems
はdiv
をレンダリングします。対照的に、Menu
とMenuItem
は要素をレンダリングしません。代わりに、デフォルトでは直接子要素をレンダリングします。
as
プロップを使用して、コンポーネントを異なる要素または独自のカスタムコンポーネントとしてレンダリングします。カスタムコンポーネントがforward refsを使用していることを確認して、Headless UIが正しく接続できるようにします。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { forwardRef } from 'react'
let MyCustomButton = forwardRef(function (props, ref) { return <button className="..." ref={ref} {...props} />})
function Example() {
return (
<Menu>
<MenuButton as={MyCustomButton}>My account</MenuButton> <MenuItems anchor="bottom" as="section"> <MenuItem as="a" className="block data-[focus]:bg-blue-100" href="/settings"> Settings
</MenuItem>
<MenuItem as="a" className="block data-[focus]:bg-blue-100" href="/support"> Support
</MenuItem>
<MenuItem as="a" className="block data-[focus]:bg-blue-100" href="/license"> License
</MenuItem>
</MenuItems>
</Menu>
)
}
ラッパー要素なしで子要素を直接レンダリングする要素を指定するには、as={Fragment}
を使用します。
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { Fragment } from 'react'
function Example() {
return (
<Menu>
<MenuButton as={Fragment}> <button>My account</button> </MenuButton> <MenuItems anchor="bottom">
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/settings">
Settings
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/support">
Support
</a>
</MenuItem>
<MenuItem>
<a className="block data-[focus]:bg-blue-100" href="/license">
License
</a>
</MenuItem>
</MenuItems>
</Menu>
)
}
これは、MenuItem
内で<a>
タグのようなインタラクティブな要素を使用している場合に重要です。MenuItem
にas="div"
がある場合、Headless UIによって提供されるプロップはa
ではなくdiv
に転送されるため、キーボードから<a>
タグによって提供されるURLに移動できなくなります。
Next.js v13より前では、Link
コンポーネントは不明なプロップを基になるa
要素に転送しなかったため、MenuItem
内で使用した場合、クリック時にメニューが閉じませんでした。
Next.js v12以前を使用している場合は、Link
をラップし、不明なプロップを子a
要素に転送する独自のコンポーネントを作成することで、この問題を回避できます。
import { forwardRef } from 'react'
import Link from 'next/link'
import { Menu, MenuButton, MenuItems, MenuItem } 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>
<MenuButton>My account</MenuButton>
<MenuItems anchor="bottom">
<MenuItem>
<MyLink href="/settings">Settings</MyLink> </MenuItem>
</MenuItems>
</Menu>
)
}
これにより、Headless UIがa
要素に追加する必要があるすべてのイベントリスナーが正しく適用されます。
この動作はNext.js v13で変更されたため、この回避策は不要になりました。
コマンド | 説明 |
EnterまたはSpace( | メニューを開き、最初の有効な項目にフォーカスします。 |
下方向キーまたは上方向キー( | メニューを開き、最初または最後の有効な項目にフォーカスします。 |
Esc(メニューが開いている場合) | 開いているメニューをすべて閉じます。 |
下方向キーまたは上方向キー(メニューが開いている場合) | 前または次の有効な項目にフォーカスします。 |
HomeまたはPageUp(メニューが開いている場合) | 最初の有効な項目にフォーカスします。 |
EndまたはPageDown(メニューが開いている場合) | 最後の有効な項目にフォーカスします。 |
EnterまたはSpace(メニューが開いている場合) | 現在のメニュー項目をアクティブ化/クリックします。 |
A~Zまたはa~z(メニューが開いている場合) | キーボード入力に一致する最初の項目にフォーカスします。 |
プロパティ | デフォルト値 | 説明 |
as | Fragment | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニューとしてレンダリングするべきです。 |
データ属性 | レンダープロップ | 説明 |
data-open | open |
メニューが開いているかどうか。メニュー開いているかどうか。 |
— | close |
メニューを閉じ、 |
プロパティ | デフォルト値 | 説明 |
as | button | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニューボタンとしてレンダリングするべきです。 |
disabled | false | ブール値 メニューが開いているかどうか。メニューボタン無効化されているかどうか. |
データ属性 | レンダープロップ | 説明 |
data-open | open |
メニューが開いているかどうか。メニュー開いているかどうか。 |
data-focus | focus |
メニューが開いているかどうか。メニューボタンフォーカスされているかどうか。 |
data-hover | hover |
メニューが開いているかどうか。メニューボタンホバーされているかどうか。 |
data-active | active |
メニューが開いているかどうか。メニューボタンアクティブまたは押されている状態かどうか。 |
data-autofocus | autofocus |
|
プロパティ | デフォルト値 | 説明 |
as | div | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニュー項目としてレンダリングするべきです。 |
transition | false | ブール値
|
anchor | — | オブジェクト ドロップダウンがボタンにどのようにアンカーされるかを設定します。 |
anchor.to | bottom | 文字列 トリガーを基準としたメニュー項目位置。
|
anchor.gap | 0 | 数値 | 文字列 ドロップダウンとメニューボタンの間隔。メニュー項目.
|
anchor.offset | 0 | 数値 | 文字列 ドロップダウンを元の位置からメニュー項目移動する距離。
|
anchor.padding | 0 | 数値 | 文字列 ドロップダウンとビューポートの間の最小間隔。メニュー項目とビューポートの間の 最小間隔。 |
| false | ブール値 内部で管理されている開閉状態を無視するかどうか。 |
unmount | true | ブール値 開閉状態に基づいて要素をアンマウントするか非表示にするかどうか。 |
portal | false | ブール値 要素をポータルにレンダリングするかどうか。
|
modal | true | ブール値 スクロールロック、フォーカストラップ、その他の要素を不活性にするなど、アクセシビリティ機能を有効にするかどうか。 |
データ属性 | レンダープロップ | 説明 |
data-open | open |
メニューが開いているかどうか。メニュー開いているかどうか。 |
プロパティ | デフォルト値 | 説明 |
as | Fragment | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニュー項目としてレンダリングするべきです。 |
disabled | false | ブール値 メニューが開いているかどうか。メニュー項目無効化されているかどうかキーボードナビゲーションとARIAの目的のため. |
データ属性 | レンダープロップ | 説明 |
data-disabled | disabled |
メニューが開いているかどうか。メニュー項目無効化されているかどうか。 |
data-focus | focus |
メニューが開いているかどうか。メニュー項目フォーカスされているかどうか。 |
— | close |
メニューを閉じ、 |
プロパティ | デフォルト値 | 説明 |
as | div | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニューセクションとしてレンダリングするべきです。 |
プロパティ | デフォルト値 | 説明 |
as | ヘッダー | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニュー見出しとしてレンダリングするべきです。 |
プロパティ | デフォルト値 | 説明 |
as | div | 文字列 | コンポーネント メニューがレンダリングされる要素またはコンポーネント。メニューセパレータとしてレンダリングするべきです。 |
Headless UIを使用した、事前にデザインされたTailwind CSSドロップダウンコンポーネントの例に興味がある場合は、私たちによって構築された美しくデザインされ、専門的に作成されたコンポーネントのコレクションであるTailwind UIをご覧ください。
これは、このようなオープンソースプロジェクトへの取り組みを支援する優れた方法であり、プロジェクトの改善と維持を可能にします。