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 變數,進而調整不同斷點的樣式。

查看每個元件的錨點定位文件以了解所有詳細資訊。


新的核取方塊元件

我們新增了一個新的 headless 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 文件開始,然後逐步瀏覽其餘文件。


改進的懸停、焦點和作用中狀態偵測

Headless UI 使用來自很棒的 React Aria 程式庫的底層 Hook,現在會將更智慧的 data-* 狀態屬性新增到你的控制項中,這些控制項在不同裝置上的行為比原生 CSS 虛擬類別更一致

  • data-active — 類似 :active,但在將元素拖曳離開時會移除。
  • data-hover — 類似 :hover,但在觸控裝置上會忽略以避免黏滯的懸停狀態。
  • data-focus — 類似 :focus-visible,沒有來自命令式對焦的假陽性。

按一下、懸停、對焦和拖曳按鈕以查看套用的資料屬性

若要深入了解為何使用 JavaScript 套用這些樣式很重要,我強烈建議閱讀 Devon Govett 關於此主題的出色部落格系列

為了實際做出美好的事物,網路所需付出的努力程度總是讓我感到驚訝。


組合方塊列表虛擬化

我們已將 TanStack Virtual 整合到 Headless UI 中,以在需要將十萬個項目放入組合方塊時支援清單虛擬化,因為,嘿,老闆就是這樣告訴你的。

使用新的 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 以查看!

直接將我們所有的更新發送到你的收件匣。
訂閱我們的電子報。