Headless UI v2.0 for React

Adam Wathan
Jonathan Reinink
Headless UI v2.0

没有什么比使用自己的工具构建真实的东西更能找到改进方法了。

在过去几个月里,我们一直在开发 Catalyst,我们对 Headless UI 进行了许多改进,让您可以编写更少的代码,并使开发者体验更好。

我们刚刚发布了 Headless UI v2.0 for React,这是所有这些工作的结晶。

以下是所有最好的新内容:

通过从 npm 安装最新版本的 @headlessui/react 将其添加到您的项目中:

npm install @headlessui/react@latest

如果您是从 v1.x 升级,请查看升级指南以了解更多有关更改的信息。


内置锚点定位

我们已将 Floating UI 直接集成到 Headless UI 中,因此您无需担心下拉菜单超出视图或被屏幕上的其他元素遮挡。

使用 MenuPopoverComboboxListbox 组件上的新 anchor 属性来指定锚点定位,然后使用 CSS 变量(如 --anchor-gap--anchor-padding)微调位置:

Scroll up and down to see the dropdown position change

import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";function Example() {  return (    <Menu>      <MenuButton>Options</MenuButton>      <MenuItems        anchor="bottom start"        className="[--anchor-gap:8px] [--anchor-padding:8px]"      >        <MenuItem>          <button>Edit</button>        </MenuItem>        <MenuItem>          <button>Duplicate</button>        </MenuItem>        <hr />        <MenuItem>          <button>Archive</button>        </MenuItem>        <MenuItem>          <button>Delete</button>        </MenuItem>      </MenuItems>    </Menu>  );}

这个 API 真正的好处是,您可以通过使用实用程序类(如 sm:[--anchor-gap:4px])更改 CSS 变量,从而在不同的断点调整样式。

查看每个组件的锚点定位文档以获取所有详细信息。


新的复选框组件

我们添加了一个新的 headless Checkbox 组件来补充我们现有的 RadioGroup 组件,从而可以轻松构建完全自定义的复选框控件:

This will give you early access to any awesome new features we're developing.

import { Checkbox, Description, Field, Label } from "@headlessui/react";import { CheckmarkIcon } from "./icons/checkmark";import clsx from "clsx";function Example() {  return (    <Field>      <Checkbox        defaultChecked        className={clsx(          "size-4 rounded border bg-white dark:bg-white/5",          "data-[checked]:border-transparent data-[checked]:bg-blue-500",          "focus:outline-none data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500",        )}      >        <CheckmarkIcon className="stroke-white opacity-0 group-data-[checked]:opacity-100" />      </Checkbox>      <div>        <Label>Enable beta features</Label>        <Description>This will give you early access to any awesome new features we're developing.</Description>      </div>    </Field>  );}

复选框可以是受控的或非受控的,并且可以自动将其状态与隐藏的输入同步,以便与 HTML 表单完美配合。

查看 Checkbox 文档以了解更多信息。


HTML 表单组件

我们添加了一整套新的组件,这些组件只是包装了原生表单控件,但会自动为您完成所有繁琐的 ID 和 aria-* 属性的连接工作。

以下是构建一个带有正确关联的 <label> 和描述的简单 <input> 字段的样子:

<div>  <label id="name-label" for="name-input">    Name  </label>  <input id="name-input" aria-labelledby="name-label" aria-describedby="name-description" />  <p id="name-description">Use your real name so people will recognize you.</p></div>

以下是使用 Headless UI v2.0 中这些新组件的样子:

import { Description, Field, Input, Label } from "@headlessui/react";function Example() {  return (    <Field>      <Label>Name</Label>      <Input name="your_name" />      <Description>Use your real name so people will recognize you.</Description>    </Field>  );}

新的 FieldFieldset 组件还会像原生 <fieldset> 元素一样级联禁用状态,因此您可以轻松地一次禁用一组控件:

Select a country to see the region field become enabled

Shipping details

We currently only ship to North America.

import { Button, Description, Field, Fieldset, Input, Label, Legend, Select } from "@headlessui/react";import { regions } from "./countries";export function Example() {  const [country, setCountry] = useState(null);  return (    <form action="/shipping">      <Fieldset>        <Legend>Shipping details</Legend>        <Field>          <Label>Street address</Label>          <Input name="address" />        </Field>        <Field>          <Label>Country</Label>          <Description>We currently only ship to North America.</Description>          <Select name="country" value={country} onChange={(event) => setCountry(event.target.value)}>            <option></option>            <option>Canada</option>            <option>Mexico</option>            <option>United States</option>          </Select>        </Field>        <Field disabled={!country}>          <Label className="data-[disabled]:opacity-40">State/province</Label>          <Select name="region" className="data-[disabled]:opacity-50">            <option></option>            {country && regions[country].map((region) => <option>{region}</option>)}          </Select>        </Field>        <Button>Submit</Button>      </Fieldset>    </form>  );}

我们在渲染的 HTML 中使用 data-disabled 属性公开禁用状态。这样即使在不支持原生 disabled 属性的元素(如关联的 <label> 元素)上也可以公开它,从而可以非常轻松地微调每个元素的禁用样式。

总的来说,我们在这里添加了 8 个新组件 — FieldsetLegendFieldLabelDescriptionInputSelectTextarea

有关更多详细信息,请从 Fieldset 文档 开始,然后逐步了解其余内容。


改进的悬停、焦点和活动状态检测

使用来自优秀的 React Aria 库的钩子,Headless UI 现在会向您的控件添加更智能的 data-* 状态属性,这些属性在不同设备上的行为比原生 CSS 伪类更一致:

  • data-active — 类似于 :active,但在拖动离开元素时会被移除。
  • data-hover — 类似于 :hover,但在触摸设备上会被忽略以避免粘滞的悬停状态。
  • data-focus — 类似于 :focus-visible,但没有来自命令性聚焦的误报。

Click, hover, focus, and drag the button to see the data attributes applied

要了解为什么使用 JavaScript 应用这些样式很重要,我强烈建议阅读 Devon Govett 在此主题上的优秀博客系列:

网络从未停止让我惊讶于实际上制作漂亮的东西所需的努力。


Combobox 列表虚拟化

我们已将 TanStack Virtual 集成到 Headless UI 中,以支持列表虚拟化,当您需要在组合框中放置十万个项目时,因为,嘿,这是老板让您做的。

使用新的 virtual 属性传入所有项目,并使用 ComboboxOptions 渲染属性提供单个选项的模板:

Open the combobox and scroll through the 1,000 options

import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react";import { ChevronDownIcon } from "@heroicons/react/20/solid";import { useState } from "react";const people = [  { id: 1, name: "Rossie Abernathy" },  { id: 2, name: "Juana Abshire" },  { id: 3, name: "Leonel Abshire" },  { id: 4, name: "Llewellyn Abshire" },  { id: 5, name: "Ramon Abshire" },  // ...up to 1000 people];function Example() {  const [query, setQuery] = useState("");  const [selected, setSelected] = useState(people[0]);  const filteredPeople =    query === ""      ? people      : people.filter((person) => {          return person.name.toLowerCase().includes(query.toLowerCase());        });  return (    <Combobox      value={selected}      virtual={{ options: filteredPeople }}      onChange={(value) => setSelected(value)}      onClose={() => setQuery("")}    >      <div>        <ComboboxInput displayValue={(person) => person?.name} onChange={(event) => setQuery(event.target.value)} />        <ComboboxButton>          <ChevronDownIcon />        </ComboboxButton>      </div>      <ComboboxOptions>        {({ option: person }) => (          <ComboboxOption key={person.id} value={person}>            {person.name}          </ComboboxOption>        )}      </ComboboxOptions>    </Combobox>  );}

查看新的虚拟滚动文档以了解更多信息。


新网站和改进的文档

为了配合这个主要版本,我们还对文档进行了重大改进,并为网站添加了新的外观:

新的 Headless UI v2.0 网站

前往新的 headlessui.com 查看!

Get all of our updates directly to your inbox.
Sign up for our newsletter.