下拉功能表
功能表提供簡易方式來建置自訂的、無障礙的下拉式元件,且能 robust 地支援鍵盤導覽。
要開始使用,透過 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 會追蹤每個組件的大量狀態,例如目前是哪個功能表項目透過鍵盤取得焦點、快顯視窗是開啟還是關閉,或是目前已選擇哪個列表方塊選項。
但由於這些組件是 Headless,而且完全沒有內建樣式,因此在你自行提供每種狀態所需的樣式之前,你無法在你的使用者介面中看到這些資訊。
設定 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>
)
}
將 anchor
屬性新增至 MenuItems
,以自動將下拉選單定位在 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
屬性還支援一個物件 API,讓您可以使用 JavaScript 控制 gap
、offset
和 padding
值
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` 元件的實作完全相同。請參閱 過場動畫文件 了解更多。
Headless UI 還能很好地與 React 生態系統中的其他動畫函式庫組成,例如 Framer Motion 和 React Spring。你只需要向這些函式庫提供一些狀態即可。
舉例來說,若要使用 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` 屬性將元件呈現為不同的元素或你自己的自訂元件,確保你的自訂元件 傳遞參照,以便 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 提供的屬性會傳遞給 div
而不是 a
,這表示您無法再透過鍵盤前往 <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在 | 開啟選單,並將焦點移至第一個未停用的項目 |
ArrowDown 或 ArrowUp在 | 開啟選單,並將焦點移至第一個/最後一個未停用的項目 |
Esc在選單開啟時 | 關閉任何開啟的選單 |
ArrowDown 或 ArrowUp在選單開啟時 | 焦點前一個/後一個非停用項目 |
首頁 或 向前翻頁當選單開啟時 | 焦點第一個非停用項目 |
結束 或 向後翻頁當選單開啟時 | 焦點最後一個非停用項目 |
輸入 或 空格當選單開啟時 | 啟動/點擊目前選單項目 |
A–Z 或 a–z當選單開啟時 | 焦點第一個與鍵盤輸入相符的項目 |
Prop | 預設值 | 說明 |
as | 片段 | 字串 | 元件 該元素或元件選單應渲染為。 |
資料屬性 | 渲染 Prop | 說明 |
資料-開啟 | 開啟 |
是否選單開啟。 |
— | 關閉 |
關閉選單並重新對焦 |
Prop | 預設值 | 說明 |
as | 按鈕 | 字串 | 元件 該元素或元件選單按鈕應渲染為。 |
已停用 | false | 布林值 是否選單按鈕已停用. |
資料屬性 | 渲染 Prop | 說明 |
資料-開啟 | 開啟 |
是否選單開啟。 |
資料-焦點 | 焦點 |
是否選單按鈕已對焦。 |
資料-游標 | 游標 |
是否選單按鈕已游標。 |
資料-動作中 | 動作中 |
是否選單按鈕處於動作中或按下狀態。 |
資料-自動對焦 | 自動對焦 |
|
Prop | 預設值 | 說明 |
as | div | 字串 | 元件 該元素或元件選單項目應渲染為。 |
過渡 | false | 布林值 該元素是否應渲染過渡屬性,例如 |
錨點 | — | 物件 設定下拉選單固定到按鈕的方式。 |
錨點。至 | 底部 | 字串 在哪裡配置選單項目相對於觸發器。 使用值 |
anchor.gap | 0 | 數字 | 字串 中間的空間選單按鈕與選單項目. 可以使用 |
anchor.offset | 0 | 數字 | 字串 距離選單項目應從其原始位置輕推。 可以使用 |
anchor.padding | 0 | 數字 | 字串 最小空間選單項目與 視窗。可以使用 |
static | false | 布林值 元素是否應略過內部管理的開啟/關閉狀態。 |
unmount | true | 布林值 元素是否應基於開啟/關閉狀態解除安裝或隱藏。 |
portal | false | 布林值 元素是否應在傳送門中呈現。 設定 |
modal | true | 布林值 是否啟用協助無障礙功能,例如捲動鎖定、焦點鎖定,以及讓其他元素 |
資料屬性 | 渲染 Prop | 說明 |
資料-開啟 | 開啟 |
是否選單開啟。 |
Prop | 預設值 | 說明 |
as | 片段 | 字串 | 元件 該元素或元件選單項目應渲染為。 |
已停用 | false | 布林值 是否選單項目已停用用於鍵盤導覽和 ARIA 功能. |
資料屬性 | 渲染 Prop | 說明 |
資料-已停用 | 已停用 |
是否選單項目已停用。 |
資料-焦點 | 焦點 |
是否選單項目已對焦。 |
— | 關閉 |
關閉選單並重新對焦 |
Prop | 預設值 | 說明 |
as | div | 字串 | 元件 該元素或元件選單區段應渲染為。 |
Prop | 預設值 | 說明 |
as | header | 字串 | 元件 該元素或元件選單標題應渲染為。 |
Prop | 預設值 | 說明 |
as | div | 字串 | 元件 該元素或元件選單分隔器應渲染為。 |
如果您有興趣使用 Headless UI 的預先設計 Tailwind CSS 下拉式選單元件範例請查看 Tailwind UI — 由我們建置,包含設計精美且製作精良元件的集合。
這是支持我們在類似這種的開源專案中工作的絕佳方式,並使我們能夠改進及妥善維護這些專案。