用于 React 的 Headless UI v2.0

Adam Wathan
Jonathan Reinink
Headless UI v2.0

当涉及到寻找改进方法时,没有什么比使用自己的工具构建真实的东西更有效了。

在过去的几个月中,当我们一直在开发 Catalyst 时,我们对 Headless UI 进行了数十项改进,让你可以编写更少的代码,并使开发人员的体验更好。

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

以下是所有最好的新功能

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

npm install @headlessui/react@latest

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


内置锚点定位

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

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

向上和向下滚动以查看下拉位置的变化

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 变量来在不同的断点调整样式。

查看每个组件的 锚点定位文档,了解所有详细信息。


新的复选框组件

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

这将使你能够提前访问我们正在开发的任何很棒的新功能。

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 表单良好配合。

请查看 复选框文档 以了解更多信息。


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> 元素一样级联禁用状态,因此你可以轻松地一次禁用整个控件组

选择一个国家/地区以查看区域字段变为启用状态

送货详情

我们目前只运送到北美。

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,没有来自强制聚焦的误报。

单击、悬停、聚焦和拖动按钮以查看应用的数据属性

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

Web 永远不会让我对实际制作精美事物所需付出的努力感到惊讶。


组合框列表虚拟化

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

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

打开组合框并滚动浏览 1,000 个选项

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

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


新网站和改进的文档

为了配合此次重大发布,我们还对文档进行了重大改进,并为网站赋予了全新的面貌

New Headless UI v2.0 website

前往新的 headlessui.com 查看!

将我们的所有更新直接发送到你的收件箱。
注册我们的时事通讯。