選單 (下拉式)
選單提供簡易的方法建立自訂、無障礙的下拉式元件,並提供穩健的鍵盤導覽支援。
入門前,請透過 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()
,這會阻止預設行為,進而導致選單未關閉。
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 元件並不會渲染,因此它們可以與 React 生態系統中的其他動畫函式庫很好地組合使用,例如 Framer Motion 和 React Spring。
role="menu"
的輔助使用者介面語義相當嚴格,Menu
的任何子項,如果不是 Menu.Item
元件,都會自動對輔助技術隱藏,以確保選單正確運作,就像螢幕閱讀器使用者所預期的那樣。
基於這個原因,建議不要渲染 Menu.Item
元件以外的子項,因為該內容對於使用輔助技術的人員來說是無法存取的。
如果你想要建立具有更彈性內容的下拉選單,請考慮改用 浮動視窗。
預設情況下,Menu
及其子元件各自會渲染適合該元件的預設元素。
例如,Menu.Button
預設會渲染一個 button
,而 Menu.Items
會渲染一個 div
。相對地,Menu
和 Menu.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.Item
有 as="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 } = 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
會切換選單。點擊開啟的選單外的任何地方都會關閉該選單。
指令 | 說明 |
Enter 或 Space,當 | 開啟選單並將焦點集中於第一個未停用的項目 |
ArrowDown 或 ArrowUp當 | 開啟選單並將焦點集中於第一個/最後一個未停用的項目 |
Esc,當選單開啟時 | 關閉任何已開啟的選單 |
ArrowDown 或 ArrowUp當選單開啟時 | 將焦點集中於上一個/下一個未停用的項目 |
首頁 或 PageUp 當功能表開啟時 | 在第一個未停用的項目上對焦 |
結束 或 PageDown 當功能表開啟時 | 在最後一個未停用的項目上對焦 |
Enter 或 空白鍵 當功能表開啟時 | 啟用/按一下當前功能表項目 |
A–Z 或 a–z 當功能表開啟時 | 在符合鍵盤輸入的第一個項目上對焦 |
所有相關 ARIA 屬性均自動管理。
要完整了解在 Menu
中實作的所有輔助功能,請參閱 功能表按鈕上的 ARIA 規範。
功能表最適合用於類似於大多數作業系統標題列中功能表的 UI 元素。它們具有特定的輔助功能語意,其內容應僅限於連結或按鈕清單。焦點會停留在已開啟的功能表中,因此您無法使用 Tab 鍵瀏覽內容或離開功能表。相反地,箭頭鍵會在功能表的項目中導覽。
以下說明何時可以使用 Headless UI 中其他類似的元件
-
<Popover />
。浮動資訊是通用的浮動功能表。它們出現在觸發它們的按鈕附近,您可以在其中放置任意標記,例如圖像或不可按的內容。Tab 鍵會像導覽其他一般標記一樣導覽浮動資訊的內容。它們非常適合建立具有可擴充內容和飛出式面板的標題導覽項目。 -
<Disclosure />
。提示是對元素非常有用,可以展開顯示額外資訊,例如可切換的常見問題解答區段。它們通常會內嵌顯示,並會在顯示或隱藏時重新整理文件。 -
<Dialog />
。對話框的目的是吸引使用者的全部注意力。它們通常會在螢幕中心呈現浮動面板,並使用背景來調暗應用程式中其他內容。它們也會擷取焦點,並防止在對話框關閉之前離開對話框內容。
屬性(Prop) | 預設值 | 說明 |
as | 區段(Fragment) | 字串 | 元件
|
渲染屬性(Render Prop) | 說明 |
open |
選單是否開啟。 |
close |
關閉選單,並重新聚焦到 |
屬性(Prop) | 預設值 | 說明 |
as | button | 字串 | 元件
|
渲染屬性(Render Prop) | 說明 |
open |
選單是否開啟。 |
屬性(Prop) | 預設值 | 說明 |
as | div | 字串 | 元件
|
static | false | 布林值 元素是否應略過內部管理的開啟/關閉狀態。 請注意: |
unmount | true | 布林值 元素是否應根據開啟/關閉狀態取消安裝或隱藏。 請注意: |
渲染屬性(Render Prop) | 說明 |
open |
選單是否開啟。 |
屬性(Prop) | 預設值 | 說明 |
as | 區段(Fragment) | 字串 | 元件
|
disabled | false | 布林值 項目是否應禁用以進行鍵盤導覽和 ARIA 目的。 |
渲染屬性(Render Prop) | 說明 |
active |
項目是否為清單中已啟動/已聚焦的項目。 |
disabled |
項目是否應禁用以進行鍵盤導覽和 ARIA 目的。 |
close |
關閉選單,並重新聚焦到 |