下拉功能表

功能表提供簡易方式來建置自訂的、無障礙的下拉式元件,且能 robust 地支援鍵盤導覽。

要開始使用,透過 npm 安裝 Headless UI

npm install @headlessui/react

功能表是使用 MenuMenuButtonMenuItems,及 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> ) }

使用 MenuSectionMenuHeadingMenuSeparator 組件,將項目根據標籤群組為區段

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> ) }

使用值 toprightbottomleft 將下拉選單置中在適當邊緣,或與 startend 結合使用,將下拉選單對齊到特定角,例如 top startbottom 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 控制 gapoffsetpadding

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 MotionReact 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 中變更,因此不再需要此解決方案。

指令說明

EnterSpaceMenuButton 取得焦點時

開啟選單,並將焦點移至第一個未停用的項目

ArrowDownArrowUpMenuButton 取得焦點時

開啟選單,並將焦點移至第一個/最後一個未停用的項目

Esc在選單開啟時

關閉任何開啟的選單

ArrowDownArrowUp在選單開啟時

焦點前一個/後一個非停用項目

首頁向前翻頁當選單開啟時

焦點第一個非停用項目

結束向後翻頁當選單開啟時

焦點最後一個非停用項目

輸入空格當選單開啟時

啟動/點擊目前選單項目

A–Za–z當選單開啟時

焦點第一個與鍵盤輸入相符的項目

Prop預設值說明
as片段
字串 | 元件

該元素或元件選單應渲染為。

資料屬性渲染 Prop說明
資料-開啟開啟

布林值

是否選單開啟。

關閉

() => void

關閉選單並重新對焦 MenuButton

Prop預設值說明
as按鈕
字串 | 元件

該元素或元件選單按鈕應渲染為。

已停用false
布林值

是否選單按鈕已停用.

資料屬性渲染 Prop說明
資料-開啟開啟

布林值

是否選單開啟。

資料-焦點焦點

布林值

是否選單按鈕已對焦。

資料-游標游標

布林值

是否選單按鈕已游標。

資料-動作中動作中

布林值

是否選單按鈕處於動作中或按下狀態。

資料-自動對焦自動對焦

布林值

autoFocus Prop 是否設為 true

Prop預設值說明
asdiv
字串 | 元件

該元素或元件選單項目應渲染為。

過渡false
布林值

該元素是否應渲染過渡屬性,例如 data-closed data-enterdata-leave

錨點
物件

設定下拉選單固定到按鈕的方式。

錨點。至底部
字串

在哪裡配置選單項目相對於觸發器。

使用值 toprightbottomleft 選單項目沿著適當邊緣,或結合 startend 以對齊選單項目指定角落,例如 top startbottom end

anchor.gap0
數字 | 字串

中間的空間選單按鈕選單項目.

可以使用 --anchor-gap CSS 變數控制。

anchor.offset0
數字 | 字串

距離選單項目應從其原始位置輕推。

可以使用 --anchor-offset CSS 變數控制。

anchor.padding0
數字 | 字串

最小空間選單項目

視窗。可以使用 --anchor-padding CSS 變數控制。

staticfalse
布林值

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

unmounttrue
布林值

元素是否應基於開啟/關閉狀態解除安裝或隱藏。

portalfalse
布林值

元素是否應在傳送門中呈現。

設定 anchor prop 時自動設定為 true

modaltrue
布林值

是否啟用協助無障礙功能,例如捲動鎖定、焦點鎖定,以及讓其他元素 inert.

資料屬性渲染 Prop說明
資料-開啟開啟

布林值

是否選單開啟。

Prop預設值說明
as片段
字串 | 元件

該元素或元件選單項目應渲染為。

已停用false
布林值

是否選單項目已停用用於鍵盤導覽和 ARIA 功能.

資料屬性渲染 Prop說明
資料-已停用已停用

布林值

是否選單項目已停用。

資料-焦點焦點

布林值

是否選單項目已對焦。

關閉

() => void

關閉選單並重新對焦 MenuButton

MenuItem component 清單分為具有適當協助無障礙語意的區段。

Prop預設值說明
asdiv
字串 | 元件

該元素或元件選單區段應渲染為。

加入標籤至 MenuSection 中。

Prop預設值說明
asheader
字串 | 元件

該元素或元件選單標題應渲染為。

以適當的協助無障礙語意,將 MenuSection 元件分開。

Prop預設值說明
asdiv
字串 | 元件

該元素或元件選單分隔器應渲染為。

如果您有興趣使用 Headless UI 的預先設計 Tailwind CSS 下拉式選單元件範例請查看 Tailwind UI — 由我們建置,包含設計精美且製作精良元件的集合。

這是支持我們在類似這種的開源專案中工作的絕佳方式,並使我們能夠改進及妥善維護這些專案。