菜单 (下拉菜单)
菜单提供了一种简单的方法来构建自定义的、可访问的下拉组件,并提供对键盘导航的强大支持。
要开始,请通过 npm 安装无头 UI
npm install @headlessui/react
菜单按钮使用 Menu
、Menu.Button
、Menu.Items
和 Menu.Item
组件构建。
Menu.Button
会在点击时自动打开/关闭 Menu.Items
,当菜单打开时,项目列表会获得焦点,并可以通过键盘自动导航。
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> ) }
无头 UI 跟踪每个组件的大量状态,例如当前选中的列表框选项、弹出窗口是打开还是关闭,或者菜单中哪个项目当前通过键盘激活。
但由于组件是无头的,并且在开箱即用时完全没有样式,因此您无法看到此信息在您的 UI 中,直到您自己为每个状态提供您想要的样式。
每个组件通过 渲染道具 公开其当前状态的信息,您可以使用它来有条件地应用不同的样式或渲染不同的内容。
例如,Menu.Item
组件公开一个 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
时,它们将以空格分隔的字符串的形式列在这个属性中,以便您可以使用 CSS 属性选择器 以 [attr~=value]
的形式来定位它们。
例如,以下是 Menu.Items
组件与一些子 Menu.Item
组件在菜单打开且第二个项目为 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> ) }
由于它们是无渲染的,无头 UI 组件还可以很好地与 React 生态系统中的其他动画库(如 Framer Motion 和 React Spring)组合使用。
role="menu"
的无障碍语义相当严格,任何不是 Menu.Item
组件的 Menu
子项都会被自动隐藏在辅助技术之外,以确保菜单按照屏幕阅读器用户预期的方式工作。
出于这个原因,不建议渲染除 Menu.Item
组件以外的任何子项,因为这些内容将无法被使用辅助技术的人员访问。
如果您想构建一个具有更灵活内容的下拉菜单,请考虑使用 Popover 而不是它。
默认情况下,Menu
及其子组件都会渲染一个对该组件来说是明智的默认元素。
例如,Menu.Button
默认渲染一个 button
,而 Menu.Items
渲染一个 div
。相比之下,Menu
和 Menu.Item
不渲染元素,而是默认直接渲染其子项。
使用 as
属性将组件渲染为不同的元素,或者渲染为您自己的自定义组件,确保您的自定义组件 转发 ref,以便无头 UI 可以正确地连接它们。
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"
,那么无头 UI 提供的道具将转发到 div
而不是 a
,这意味着您无法再通过键盘访问 <a>
标记提供的 URL。
在 Next.js v13 之前,Link
组件不会将未知道具转发到底层的 a
元素,这会阻止菜单在 Menu.Item
中使用时在点击时关闭。
如果您使用的是 Next.js v12 或更旧版本,您可以通过创建一个包装 Link
并将未知道具转发到底层 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> ) }
这将确保将无头 UI 需要添加到 a
元素的所有事件监听器都正确应用。
此行为在 Next.js v13 中更改,因此不再需要此解决方法。
点击 Menu.Button
会切换菜单并使 Menu.Items
组件获得焦点。焦点会一直停留在打开的菜单中,直到按下 Escape 或用户点击菜单之外。关闭菜单会将焦点返回到 Menu.Button
。
点击 Menu.Button
会切换菜单。点击打开菜单之外的任何地方都会关闭该菜单。
命令 | 描述 |
Enter 或 Space 当 | 打开菜单并使第一个未禁用的项目获得焦点 |
ArrowDown 或 ArrowUp当 | 打开菜单并使第一个/最后一个未禁用的项目获得焦点 |
Esc 当菜单打开时 | 关闭所有打开的菜单 |
ArrowDown 或 ArrowUp当菜单打开时 | 使上一个/下一个未禁用的项目获得焦点 |
Home 或 PageUp 当菜单打开时 | 使第一个未禁用的项目获得焦点 |
End 或 PageDown 当菜单打开时 | 聚焦最后一个未禁用的项目 |
Enter 或 Space 当菜单打开时 | 激活/点击当前菜单项 |
A–Z 或 a–z 当菜单打开时 | 聚焦第一个与键盘输入匹配的项目 |
所有相关的 ARIA 属性都会自动管理。
有关在 Menu
中实现的所有无障碍功能的完整参考,请参阅 菜单按钮的 ARIA 规范。
菜单最适合类似于在大多数操作系统标题栏中找到的菜单的 UI 元素。它们具有特定的无障碍语义,其内容应限于链接或按钮列表。焦点被困在打开的菜单中,因此您无法使用 Tab 键遍历内容或离开菜单。相反,箭头键在菜单的项目中导航。
以下是在何时可以使用 Headless UI 中的其他类似组件
-
<Popover />
。弹出窗口是通用的浮动菜单。它们出现在触发它们的按钮附近,您可以在其中放置任意的标记,例如图像或不可点击的内容。Tab 键以与其他正常标记相同的方式导航弹出窗口的内容。它们非常适合构建带有可扩展内容和弹出面板的标题导航项。 -
<Disclosure />
。披露对于扩展以显示其他信息的元素很有用,例如可切换的常见问题解答部分。它们通常以内联方式呈现,并在显示或隐藏时重新调整文档。 -
<Dialog />
。对话框旨在吸引用户的全部注意力。它们通常在屏幕中央呈现一个浮动面板,并使用背景来使应用程序的其余内容变暗。它们还会捕获焦点并阻止在对话框内容消失之前离开对话框的内容进行制表符切换。
属性 | 默认值 | 描述 |
as | Fragment | 字符串 | 组件
|
渲染属性 | 描述 |
open |
菜单是否打开。 |
close |
关闭菜单并重新聚焦 |
属性 | 默认值 | 描述 |
as | button | 字符串 | 组件
|
渲染属性 | 描述 |
open |
菜单是否打开。 |
属性 | 默认值 | 描述 |
as | div | 字符串 | 组件
|
static | false | 布尔值 元素是否应该忽略内部管理的打开/关闭状态。 注意: |
unmount | true | 布尔值 元素是否应该根据打开/关闭状态卸载或隐藏。 注意: |
渲染属性 | 描述 |
open |
菜单是否打开。 |
属性 | 默认值 | 描述 |
as | Fragment | 字符串 | 组件
|
disabled | false | 布尔值 项目是否应该被禁用以进行键盘导航和 ARIA 目的。 |
渲染属性 | 描述 |
active |
项目是否为列表中的活动/聚焦项目。 |
disabled |
项目是否被禁用以进行键盘导航和 ARIA 目的。 |
close |
关闭菜单并重新聚焦 |