mcpField
H
headlessui

headlessui

github.com/bobmatnyc设计与 UI78349 次安装15 周前更新
代码仓库

关于 headlessui

Headless UI 为 React 和 Vue 开发者提供无样式、易访问的 UI 组件,便于自行定义外观。组件内置键盘、屏幕阅读器和焦点支持,并兼容 TypeScript 与 SSR。

主要功能

  1. 01无样式组件基元
  2. 02键盘与屏幕阅读器支持
  3. 03自动处理 ARIA 与焦点
  4. 04支持 React 和 Vue
  5. 05兼容 TypeScript 与 SSR

适用场景

  1. 01构建设计系统
  2. 02添加易访问的菜单和对话框
  3. 03创建可搜索的选择控件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill headlessui

安装后在 Claude Code 中输入/headlessui确认已加载。

附带文件metadata.json

详细介绍

Headless UI - 无障碍组件基元

概述

Headless UI 提供完全无样式、完全无障碍的 UI 组件,旨在与 Tailwind CSS 完美集成。它由 Tailwind Labs 团队打造,在不强加设计决策的前提下,提供可用于生产环境的无障碍支持。

主要特性:

  • 完全无样式——使用你自己的样式
  • 完整的键盘导航
  • 经过屏幕阅读器测试
  • 焦点管理
  • 自动处理 ARIA 属性
  • 支持 TypeScript
  • 兼容 React 18 和 Vue 3
  • 兼容 SSR
  • 通过渲染属性实现最大灵活性

安装:

# React
npm install @headlessui/react

# Vue
npm install @headlessui/vue

组件目录

Menu(下拉菜单)

支持键盘导航和 ARIA 的无障碍下拉菜单。

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'

function DropdownMenu() {
  return (
    <Menu>
      <MenuButton className="inline-flex items-center gap-2 rounded-md bg-gray-800 py-1.5 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:outline-none data-[hover]:bg-gray-700 data-[open]:bg-gray-700 data-[focus]:outline-1 data-[focus]:outline-white">
        Options
        <ChevronDownIcon className="size-4 fill-white/60" />
      </MenuButton>

      <MenuItems
        transition
        anchor="bottom end"
        className="w-52 origin-top-right rounded-xl border border-white/5 bg-white/5 p-1 text-sm/6 text-white transition duration-100 ease-out [--anchor-gap:var(--spacing-1)] focus:outline-none data-[closed]:scale-95 data-[closed]:opacity-0"
      >
        <MenuItem>
          <button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
            Edit
          </button>
        </MenuItem>
        <MenuItem>
          <button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
            Duplicate
          </button>
        </MenuItem>
        <div className="my-1 h-px bg-white/5" />
        <MenuItem>
          <button className="group flex w-full items-center gap-2 rounded-lg py-1.5 px-3 data-[focus]:bg-white/10">
            Delete
          </button>
        </MenuItem>
      </MenuItems>
    </Menu>
  )
}

Menu 特性:

  • 方向键导航
  • 按键搜索
  • 自动管理焦点
  • 按 Escape 关闭
  • 点击外部区域关闭
  • 通过 Portal 渲染以便定位
  • 锚点定位 API
Listbox(选择框)

提供完整键盘支持的自定义选择框/下拉组件。

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Wade Cooper' },
  { id: 2, name: 'Arlene Mccoy' },
  { id: 3, name: 'Devon Webb' },
]

function SelectExample() {
  const [selected, setSelected] = useState(people[0])

  return (
    <Listbox value={selected} onChange={setSelected}>
      <ListboxButton className="relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
        <span className="block truncate">{selected.name}</span>
        <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
          <ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
        </span>
      </ListboxButton>

      <ListboxOptions className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
        {people.map((person) => (
          <ListboxOption
            key={person.id}
            value={person}
            className="relative cursor-default select-none py-2 pl-10 pr-4 data-[focus]:bg-amber-100 data-[focus]:text-amber-900"
          >
            {({ selected }) => (
              <>
                <span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
                  {person.name}
                </span>
                {selected && (
                  <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
                    <CheckIcon className="h-5 w-5" aria-hidden="true" />
                  </span>
                )}
              </>
            )}
          </ListboxOption>
        ))}
      </ListboxOptions>
    </Listbox>
  )
}

Listbox 特性:

  • 单选和多选模式
  • 按键搜索
  • 方向键导航
  • 受控和非受控模式
  • 支持禁用选项
  • 自定义值比较
Combobox(自动补全)

支持搜索和筛选的选择组件。

import { Combobox, ComboboxInput, ComboboxOptions, ComboboxOption } from '@headlessui/react'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Wade Cooper' },
  { id: 2, name: 'Arlene Mccoy' },
  { id: 3, name: 'Devon Webb' },
  { id: 4, name: 'Tom Cook' },
]

function AutocompleteExample() {
  const [selected, setSelected] = useState(people[0])
  const [query, setQuery] = useState('')

  const filtered =
    query === ''
      ? people
      : people.filter((person) =>
          person.name.toLowerCase().includes(query.toLowerCase())
        )

  return (
    <Combobox value={selected} onChange={setSelected}>
      <ComboboxInput
        className="w-full rounded-lg border-none bg-white/5 py-1.5 pr-8 pl-3 text-sm/6 text-white focus:outline-none data-[focus]:outline-2 data-[focus]:-outline-offset-2 data-[focus]:outline-white/25"
        displayValue={(person) => person?.name}
        onChange={(event) => setQuery(event.target.value)}
      />

      <ComboboxOptions className="w-[var(--input-width)] rounded-xl border border-white/5 bg-white/5 p-1 [--anchor-gap:var(--spacing-1)] empty:invisible">
        {filtered.map((person) => (
          <ComboboxOption
            key={person.id}
            value={person}
            className="group flex cursor-default items-center gap-2 rounded-lg py-1.5 px-3 select-none data-[focus]:bg-white/10"
          >
            <CheckIcon className="invisible size-4 fill-white group-data-[selected]:visible" />
            <div className="text-sm/6 text-white">{person.name}</div>
          </ComboboxOption>
        ))}
      </ComboboxOptions>
    </Combobox>
  )
}

Combobox 特性:

  • 支持筛选的文本输入
  • 键盘导航
  • 可空/可选选择
  • 自定义显示值
  • 支持异步加载数据
  • 多选模式
Dialog(模态框)

支持焦点陷阱的无障碍模态对话框。

import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
import { Fragment, useState } from 'react'

function ModalExample() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open dialog</button>

      <Transition appear show={isOpen} as={Fragment}>
        <Dialog as="div" className="relative z-10" onClose={() => setIsOpen(false)}>
          <TransitionChild
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <div className="fixed inset-0 bg-black/25" />
          </TransitionChild>

          <div className="fixed inset-0 overflow-y-auto">
            <div className="flex min-h-full items-center justify-center p-4 text-center">
              <TransitionChild
                as={Fragment}
                enter="ease-out duration-300"
                enterFrom="opacity-0 scale-95"
                enterTo="opacity-100 scale-100"
                leave="ease-in duration-200"
                leaveFrom="opacity-100 scale-100"
                leaveTo="opacity-0 scale-95"
              >
                <DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
                  <DialogTitle className="text-lg font-medium leading-6 text-gray-900">
                    Payment successful
                  </DialogTitle>
                  <div className="mt-2">
                    <p className="text-sm text-gray-500">
                      Your payment has been successfully submitted.
                    </p>
                  </div>

                  <div className="mt-4">
                    <button
                      type="button"
                      className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                      onClick={() => setIsOpen(false)}
                    >
                      Got it, thanks!
                    </button>
                  </div>
                </DialogPanel>
              </TransitionChild>
            </div>
          </div>
        </Dialog>
      </Transition>
    </>
  )
}

Dialog 特性:

  • 焦点陷阱
  • 按 Escape 关闭
  • 锁定滚动
  • 关闭时恢复焦点
  • Portal 渲染
  • 支持嵌套对话框
  • 控制初始焦点
Popover

用于工具提示、下拉菜单等的浮动面板。

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function PopoverExample() {
  return (
    <Popover className="relative">
      <PopoverButton className="inline-flex items-center gap-2 rounded-md bg-gray-800 py-1.5 px-3 text-sm/6 font-semibold text-white shadow-inner shadow-white/10 focus:outline-none data-[hover]:bg-gray-700 data-[focus]:outline-1 data-[focus]:outline-white">
        Solutions
      </PopoverButton>

      <PopoverPanel
        transition
        anchor="bottom"
        className="divide-y divide-white/5 rounded-xl bg-white/5 text-sm/6 transition duration-200 ease-in-out [--anchor-gap:var(--spacing-5)] data-[closed]:-translate-y-1 data-[closed]:opacity-0"
      >
        <div className="p-3">
          <a className="block rounded-lg py-2 px-3 transition hover:bg-white/5" href="#">
            <p className="font-semibold text-white">Insights</p>
            <p className="text-white/50">Measure actions your users take</p>
          </a>
          <a className="block rounded-lg py-2 px-3 transition hover:bg-white/5" href="#">
            <p className="font-semibold text-white">Automations</p>
            <p className="text-white/50">Create your own targeted content</p>
          </a>
        </div>
      </PopoverPanel>
    </Popover>
  )
}

Popover 特性:

  • 锚点定位
  • 点击或悬停触发
  • 点击外部区域时关闭
  • 嵌套弹出框
  • 焦点管理
  • Portal 渲染
RadioGroup

无障碍单选按钮组。

import { RadioGroup, RadioGroupOption, RadioGroupLabel } from '@headlessui/react'
import { useState } from 'react'

const plans = [
  { name: 'Startup', ram: '12GB', cpus: '6 CPUs', disk: '160 GB SSD disk' },
  { name: 'Business', ram: '16GB', cpus: '8 CPUs', disk: '512 GB SSD disk' },
  { name: 'Enterprise', ram: '32GB', cpus: '12 CPUs', disk: '1024 GB SSD disk' },
]

function RadioExample() {
  const [selected, setSelected] = useState(plans[0])

  return (
    <RadioGroup value={selected} onChange={setSelected}>
      <RadioGroupLabel className="sr-only">Server size</RadioGroupLabel>
      <div className="space-y-2">
        {plans.map((plan) => (
          <RadioGroupOption
            key={plan.name}
            value={plan}
            className="relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-md focus:outline-none data-[focus]:outline-2 data-[focus]:outline-white/75 data-[checked]:bg-sky-900/75"
          >
            <div className="flex items-center justify-between">
              <div className="flex items-center">
                <div className="text-sm">
                  <RadioGroupLabel as="p" className="font-medium text-white">
                    {plan.name}
                  </RadioGroupLabel>
                  <div className="flex gap-2 text-white/50">
                    <div>{plan.ram}</div>
                    <div aria-hidden="true">&middot;</div>
                    <div>{plan.cpus}</div>
                    <div aria-hidden="true">&middot;</div>
                    <div>{plan.disk}</div>
                  </div>
                </div>
              </div>
            </div>
          </RadioGroupOption>
        ))}
      </div>
    </RadioGroup>
  )
}

RadioGroup 特性:

  • 方向键导航
  • 禁用选项
  • 自定义样式状态
  • 受控模式
  • 支持描述
Switch(切换开关)

无障碍切换开关。

import { Switch } from '@headlessui/react'
import { useState } from 'react'

function SwitchExample() {
  const [enabled, setEnabled] = useState(false)

  return (
    <Switch
      checked={enabled}
      onChange={setEnabled}
      className="group inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition data-[checked]:bg-blue-600"
    >
      <span className="size-4 translate-x-1 rounded-full bg-white transition group-data-[checked]:translate-x-6" />
    </Switch>
  )
}

Switch 特性:

  • 受控和非受控模式
  • 支持标签
  • 支持描述
  • 禁用状态
  • 支持键盘操作(按空格键切换)
Tab(标签页)

无障碍标签页导航。

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'

function TabExample() {
  const categories = [
    {
      name: 'Recent',
      posts: [
        { id: 1, title: 'Does drinking coffee make you smarter?' },
        { id: 2, title: "So you've bought coffee... now what?" },
      ],
    },
    {
      name: 'Popular',
      posts: [
        { id: 1, title: 'Is tech making coffee better or worse?' },
        { id: 2, title: 'The most innovative things happening in coffee' },
      ],
    },
  ]

  return (
    <TabGroup>
      <TabList className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
        {categories.map((category) => (
          <Tab
            key={category.name}
            className="w-full rounded-lg py-2.5 text-sm font-medium leading-5 text-blue-700 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2 data-[selected]:bg-white data-[selected]:shadow data-[hover]:bg-white/[0.12] data-[focus]:outline-1"
          >
            {category.name}
          </Tab>
        ))}
      </TabList>
      <TabPanels className="mt-2">
        {categories.map((category, idx) => (
          <TabPanel
            key={idx}
            className="rounded-xl bg-white p-3 ring-white/60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2"
          >
            <ul>
              {category.posts.map((post) => (
                <li key={post.id} className="relative rounded-md p-3 hover:bg-gray-100">
                  <h3 className="text-sm font-medium leading-5">{post.title}</h3>
                </li>
              ))}
            </ul>
          </TabPanel>
        ))}
      </TabPanels>
    </TabGroup>
  )
}

Tab 特性:

  • 方向键导航
  • 默认选中的标签页
  • 手动激活
  • 垂直/水平布局
  • 受控模式
Disclosure(折叠面板)

可展开的内容区块。

import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'
import { ChevronUpIcon } from '@heroicons/react/20/solid'

function DisclosureExample() {
  return (
    <Disclosure>
      {({ open }) => (
        <>
          <DisclosureButton className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500/75">
            <span>What is your refund policy?</span>
            <ChevronUpIcon
              className={`${open ? 'rotate-180 transform' : ''} h-5 w-5 text-purple-500`}
            />
          </DisclosureButton>
          <DisclosurePanel className="px-4 pb-2 pt-4 text-sm text-gray-500">
            If you're unhappy with your purchase for any reason, email us within 90 days and we'll refund you in full, no questions asked.
          </DisclosurePanel>
        </>
      )}
    </Disclosure>
  )
}

Disclosure 特性:

  • 受控和非受控模式
  • 默认展开状态
  • 通过渲染属性访问状态
  • 多个折叠面板(手风琴模式)
  • 使用 Transition 实现平滑动画
Transition

用于进入/离开过渡效果的动画组件。

import { Transition } from '@headlessui/react'
import { useState } from 'react'

function TransitionExample() {
  const [isShowing, setIsShowing] = useState(false)

  return (
    <>
      <button onClick={() => setIsShowing(!isShowing)}>Toggle</button>
      <Transition
        show={isShowing}
        enter="transition-opacity duration-300"
        enterFrom="opacity-0"
        enterTo="opacity-100"
        leave="transition-opacity duration-200"
        leaveFrom="opacity-100"
        leaveTo="opacity-0"
      >
        <div className="rounded-md bg-blue-500 p-4 text-white">
          I will fade in and out
        </div>
      </Transition>
    </>
  )
}

Transition 特性:

  • 基于 CSS 类的过渡效果
  • 进入/离开生命周期
  • 嵌套过渡效果(子组件协调)
  • 支持 Appears(初始挂载动画)
  • 适用于 React 18 并发模式

高级模式

渲染属性模式

访问组件状态以进行自定义渲染。

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'

function RenderPropsExample() {
  return (
    <Listbox value={selected} onChange={setSelected}>
      {({ open }) => (
        <>
          <ListboxButton>
            Options {open ? '▲' : '▼'}
          </ListboxButton>
          <ListboxOptions>
            <ListboxOption value="a">
              {({ selected, focus }) => (
                <div className={focus ? 'bg-blue-500' : ''}>
                  {selected && '✓'} Option A
                </div>
              )}
            </ListboxOption>
          </ListboxOptions>
        </>
      )}
    </Listbox>
  )
}
受控组件

完全控制组件状态。

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import { useState } from 'react'

function ControlledTabs() {
  const [selectedIndex, setSelectedIndex] = useState(0)

  return (
    <TabGroup selectedIndex={selectedIndex} onChange={setSelectedIndex}>
      <TabList>
        <Tab>Tab 1</Tab>
        <Tab>Tab 2</Tab>
        <Tab>Tab 3</Tab>
      </TabList>
      <TabPanels>
        <TabPanel>Content 1</TabPanel>
        <TabPanel>Content 2</TabPanel>
        <TabPanel>Content 3</TabPanel>
      </TabPanels>
      <button onClick={() => setSelectedIndex(0)}>Reset to first tab</button>
    </TabGroup>
  )
}
Portal 渲染

在 DOM 层级结构之外渲染组件。

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'
import { createPortal } from 'react-dom'

function PortalMenu() {
  return (
    <Menu>
      <MenuButton>Options</MenuButton>
      {createPortal(
        <MenuItems>
          <MenuItem>
            <button>Edit</button>
          </MenuItem>
          <MenuItem>
            <button>Delete</button>
          </MenuItem>
        </MenuItems>,
        document.body
      )}
    </Menu>
  )
}
表单集成

与 React Hook Form 等表单库搭配使用。

import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'
import { useForm, Controller } from 'react-hook-form'

function FormExample() {
  const { control, handleSubmit } = useForm()

  const onSubmit = (data) => {
    console.log(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="country"
        control={control}
        rules={{ required: true }}
        render={({ field }) => (
          <Listbox {...field}>
            <ListboxButton>Select country</ListboxButton>
            <ListboxOptions>
              <ListboxOption value="us">United States</ListboxOption>
              <ListboxOption value="ca">Canada</ListboxOption>
              <ListboxOption value="mx">Mexico</ListboxOption>
            </ListboxOptions>
          </Listbox>
        )}
      />
      <button type="submit">Submit</button>
    </form>
  )
}

Vue 支持

Headless UI 在 Vue 3 中的工作方式完全相同。

<script setup>
import { ref } from 'vue'
import {
  Listbox,
  ListboxButton,
  ListboxOptions,
  ListboxOption,
} from '@headlessui/vue'

const people = [
  { id: 1, name: 'Wade Cooper' },
  { id: 2, name: 'Arlene Mccoy' },
  { id: 3, name: 'Devon Webb' },
]

const selectedPerson = ref(people[0])
</script>

<template>
  <Listbox v-model="selectedPerson">
    <ListboxButton>{{ selectedPerson.name }}</ListboxButton>
    <ListboxOptions>
      <ListboxOption
        v-for="person in people"
        :key="person.id"
        :value="person"
        v-slot="{ active, selected }"
      >
        <li :class="{ 'bg-blue-500': active }">
          {{ selected ? '✓' : '' }} {{ person.name }}
        </li>
      </ListboxOption>
    </ListboxOptions>
  </Listbox>
</template>

TypeScript 支持

通过 TypeScript 实现完整的类型安全。

import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'

interface User {
  id: number
  name: string
  role: 'admin' | 'user'
}

interface UserMenuProps {
  user: User
  onEdit: (user: User) => void
  onDelete: (userId: number) => void
}

function UserMenu({ user, onEdit, onDelete }: UserMenuProps) {
  return (
    <Menu as="div" className="relative">
      <MenuButton className="btn">{user.name}</MenuButton>
      <MenuItems className="menu">
        <MenuItem>
          {({ focus }) => (
            <button
              className={focus ? 'bg-blue-500' : ''}
              onClick={() => onEdit(user)}
            >
              Edit
            </button>
          )}
        </MenuItem>
        <MenuItem>
          {({ focus }) => (
            <button
              className={focus ? 'bg-red-500' : ''}
              onClick={() => onDelete(user.id)}
            >
              Delete
            </button>
          )}
        </MenuItem>
      </MenuItems>
    </Menu>
  )
}

Tailwind CSS 集成

Headless UI 专为 Tailwind CSS 设计。

用于状态的数据属性

Headless UI v2 使用数据属性设置状态样式。

// Modern approach with data attributes
<MenuButton className="data-[active]:bg-blue-500 data-[disabled]:opacity-50">
  Options
</MenuButton>

// Available states
// data-[active] - Element is active/focused
// data-[selected] - Element is selected
// data-[disabled] - Element is disabled
// data-[open] - Element/panel is open
// data-[focus] - Element has focus
// data-[checked] - Element is checked (Switch)
Tailwind 插件

配置 Tailwind 以支持 Headless UI 状态。

// tailwind.config.js
module.exports = {
  plugins: [
    require('@headlessui/tailwindcss')
  ]
}

现在使用修饰符:

<MenuButton className="ui-active:bg-blue-500 ui-disabled:opacity-50">
  Options
</MenuButton>

无障碍功能

ARIA 属性

所有 ARIA 属性均由系统自动管理:

  • 披露按钮上的 aria-expanded
  • 标签页/选项元素上的 aria-selected
  • 开关上的 aria-checked
  • 用于关联的 aria-labelledby
  • 用于描述的 aria-describedby
  • role 属性(menu、listbox、dialog 等)
键盘导航

内置完整的键盘支持:

  • 方向键:浏览选项/标签页
  • Enter/Space:选择/激活
  • Escape:关闭菜单/对话框
  • Tab:管理焦点
  • Home/End:跳转到第一项/最后一项(适用时)
  • 按键搜索:通过输入进行搜索
焦点管理

自动处理焦点:

  • 关闭时恢复焦点(Dialog、Menu、Popover)
  • 在模态框中锁定焦点
  • 控制初始焦点
  • 打开时跳转到获得焦点的元素
屏幕阅读器支持

已通过以下屏幕阅读器测试:

  • NVDA(Windows)
  • JAWS(Windows)
  • VoiceOver(macOS、iOS)
  • TalkBack(Android)

服务端渲染

完全兼容 Next.js、Remix 和其他 SSR 框架。

// app/page.tsx (Next.js 13+)
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'

export default function Page() {
  return (
    <Menu>
      <MenuButton>Options</MenuButton>
      <MenuItems>
        <MenuItem>
          <button>Edit</button>
        </MenuItem>
      </MenuItems>
    </Menu>
  )
}

无需特殊配置——组件在服务端和客户端上的运行方式完全一致。

最佳实践

  1. 始终提供标签——使用 sr-only 类隐藏标签
  2. 为所有状态添加样式——使用数据属性处理活动、已选中、已禁用状态
  3. 测试键盘导航——确认 Tab、方向键、Enter、Escape 均可正常使用
  4. 使用语义化 HTML——让组件按需渲染为合适的元素
  5. 提供焦点指示器——始终为键盘用户显示焦点状态
  6. 使用屏幕阅读器测试——确认播报内容正确
  7. 处理加载状态——在异步操作期间显示合适的 UI
  8. 按需使用受控模式——适用于复杂的状态管理
  9. 结合 Transition 使用——为打开/关闭添加流畅动画
  10. 通过 Portal 渲染覆盖层——使用 Portal 渲染菜单/对话框,避免 z-index 问题

常见问题

❌ 缺少用于状态的 Tailwind 类:

// WRONG - no visual feedback
<MenuButton>Options</MenuButton>

// CORRECT
<MenuButton className="data-[active]:bg-blue-500 data-[open]:bg-blue-600">
  Options
</MenuButton>

❌ 渲染属性未使用 Fragment:

// WRONG - adds extra div
<Transition show={isOpen}>
  <div>Content</div>
</Transition>

// CORRECT
<Transition show={isOpen} as={Fragment}>
  <div>Content</div>
</Transition>

❌ 忘记处理受控状态:

// WRONG - onChange does nothing
<Listbox value={selected}>
  <ListboxOptions>...</ListboxOptions>
</Listbox>

// CORRECT
<Listbox value={selected} onChange={setSelected}>
  <ListboxOptions>...</ListboxOptions>
</Listbox>

资源

总结

  • Headless UI 提供无样式、易于访问的组件原语
  • 零运行时 CSS——使用 Tailwind 或自定义 CSS 添加样式
  • 全面的无障碍支持——内置 ARIA、键盘导航和屏幕阅读器支持
  • React 和 Vue——两个框架的 API 完全一致
  • TypeScript——包含完整的类型定义
  • 渲染属性——访问组件状态以自定义渲染
  • 兼容 SSR——可与 Next.js、Remix、Nuxt 配合使用
  • 适用于——自定义设计系统、Tailwind CSS 集成、易于访问的组件

评论

支持 Markdown