選單 (下拉式)

選單提供簡易的方法建立自訂、無障礙的下拉式元件,並提供穩健的鍵盤導覽支援。

入門前,請透過 npm 安裝 Headless UI

npm install @headlessui/react

選單按鈕使用 選單選單.按鈕選單.項目選單.項目 元件建置。

點擊時,選單.按鈕 將自動開啟/關閉 選單.項目,且選單開啟時,項目清單會接收焦點,並可透過鍵盤自動導覽。

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 中 看到 這些資訊。

各個元件皆透過 渲染屬性 公開其目前狀態的資訊,你可以使用這些資訊有條件套用不同樣式或呈現不同內容。

例如,選單.項目 元件公開一個 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 屬性選取器 來鎖定它們。

例如,以下為包含一些子 Menu.Item 元件的 Menu.Items 元件,在選單開啟且第二個項目為 active 時呈現的樣子:

<!-- 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 元件並不會渲染,因此它們可以與 React 生態系統中的其他動畫函式庫很好地組合使用,例如 Framer MotionReact Spring

role="menu" 的輔助使用者介面語義相當嚴格,Menu 的任何子項,如果不是 Menu.Item 元件,都會自動對輔助技術隱藏,以確保選單正確運作,就像螢幕閱讀器使用者所預期的那樣。

基於這個原因,建議不要渲染 Menu.Item 元件以外的子項,因為該內容對於使用輔助技術的人員來說是無法存取的。

如果你想要建立具有更彈性內容的下拉選單,請考慮改用 浮動視窗

預設情況下,Menu 及其子元件各自會渲染適合該元件的預設元素。

例如,Menu.Button 預設會渲染一個 button,而 Menu.Items 會渲染一個 div。相對地,MenuMenu.Item 不會渲染元素,而是在預設情況下直接渲染其子項。

使用 as 屬性可以將元件渲染為不同的元素或你自己的自訂元件,請確保你的自訂元件會 傳遞 refs,以便 Headless UI 能正確地連接。p

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 提供的屬性會傳遞到 div,而不是 a,這表示你無法再透過鍵盤瀏覽到 <a> 標籤提供的網址。

在 Next.js v13 之前,連結元件不會將未知的 props 轉譯到底層的 a 元素,這使得選單在用於 Menu.Item 中時無法在點擊時關閉。

如果您使用的是 Next.js v12 或更舊版本,您可以透過建立自己的元件來處理這個問題,該元件封裝了 連結,並將未知的 props 轉譯到子元件 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 會切換選單。點擊開啟的選單外的任何地方都會關閉該選單。

指令說明

EnterSpace,當 Menu.Button 取得焦點時

開啟選單並將焦點集中於第一個未停用的項目

ArrowDownArrowUpMenu.Button 取得焦點時

開啟選單並將焦點集中於第一個/最後一個未停用的項目

Esc,當選單開啟時

關閉任何已開啟的選單

ArrowDownArrowUp當選單開啟時

將焦點集中於上一個/下一個未停用的項目

首頁PageUp 當功能表開啟時

在第一個未停用的項目上對焦

結束PageDown 當功能表開啟時

在最後一個未停用的項目上對焦

Enter空白鍵 當功能表開啟時

啟用/按一下當前功能表項目

A–Za–z 當功能表開啟時

在符合鍵盤輸入的第一個項目上對焦

所有相關 ARIA 屬性均自動管理。

要完整了解在 Menu 中實作的所有輔助功能,請參閱 功能表按鈕上的 ARIA 規範

功能表最適合用於類似於大多數作業系統標題列中功能表的 UI 元素。它們具有特定的輔助功能語意,其內容應僅限於連結或按鈕清單。焦點會停留在已開啟的功能表中,因此您無法使用 Tab 鍵瀏覽內容或離開功能表。相反地,箭頭鍵會在功能表的項目中導覽。

以下說明何時可以使用 Headless UI 中其他類似的元件

  • <Popover />。浮動資訊是通用的浮動功能表。它們出現在觸發它們的按鈕附近,您可以在其中放置任意標記,例如圖像或不可按的內容。Tab 鍵會像導覽其他一般標記一樣導覽浮動資訊的內容。它們非常適合建立具有可擴充內容和飛出式面板的標題導覽項目。

  • <Disclosure />。提示是對元素非常有用,可以展開顯示額外資訊,例如可切換的常見問題解答區段。它們通常會內嵌顯示,並會在顯示或隱藏時重新整理文件。

  • <Dialog />。對話框的目的是吸引使用者的全部注意力。它們通常會在螢幕中心呈現浮動面板,並使用背景來調暗應用程式中其他內容。它們也會擷取焦點,並防止在對話框關閉之前離開對話框內容。

屬性(Prop)預設值說明
as區段(Fragment)
字串 | 元件

Menu 應呈現為的元素或元件。

渲染屬性(Render Prop)說明
open

布林值

選單是否開啟。

close

() => void

關閉選單,並重新聚焦到 Menu.Button

屬性(Prop)預設值說明
asbutton
字串 | 元件

Menu.Button 應呈現為的元素或元件。

渲染屬性(Render Prop)說明
open

布林值

選單是否開啟。

屬性(Prop)預設值說明
asdiv
字串 | 元件

Menu.Items 應呈現為的元素或元件。

staticfalse
布林值

元素是否應略過內部管理的開啟/關閉狀態。

請注意:staticunmount 無法同時使用。如果您嘗試執行此操作,會出現 TypeScript 錯誤。

unmounttrue
布林值

元素是否應根據開啟/關閉狀態取消安裝或隱藏。

請注意:staticunmount 無法同時使用。如果您嘗試執行此操作,會出現 TypeScript 錯誤。

渲染屬性(Render Prop)說明
open

布林值

選單是否開啟。

屬性(Prop)預設值說明
as區段(Fragment)
字串 | 元件

Menu.Item 應呈現為的元素或元件。

disabledfalse
布林值

項目是否應禁用以進行鍵盤導覽和 ARIA 目的。

渲染屬性(Render Prop)說明
active

布林值

項目是否為清單中已啟動/已聚焦的項目。

disabled

布林值

項目是否應禁用以進行鍵盤導覽和 ARIA 目的。

close

() => void

關閉選單,並重新聚焦到 Menu.Button

如果您有興趣使用 Headless UI 和 Tailwind CSS 預先設計的元件範例,請查看 Tailwind UI — 我們建置的一系列設計精緻、製作精良的元件。

這是支持我們進行類似此開放原始碼專案工作的一個好方法,讓我們得以改良專案,並持續進行良好的維護。