清單方塊

清單方塊是建置您的應用程式客製式可存取選單的絕佳基礎,並具備健全的鍵盤導覽支援。

開始時,透過 npm 安裝 Headless UI

npm install @headlessui/react

清單匡使用 ListboxListboxButtonListboxSelectedOptionListboxOptionsListboxOption 組件建立。

點選 ListboxButton 會自動開啟/關閉 ListboxOptions,而且在清單匡開啟時,該選項清單會獲得焦點,而且可以使用鍵盤自動瀏覽。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
          <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
            {person.name}
          </ListboxOption>
        ))}
      </ListboxOptions>
    </Listbox>
  )
}

Headless UI 會追蹤每個組件的許多狀態,例如目前選擇哪一個清單匡選項、彈出視窗是否開啟或關閉,或使用鍵盤時目前對選單中的哪一個項目設有焦點。

然而由於組件是無頭的,而且開箱即用時完全沒有樣式,因此在您提供您想要的每一個狀態的樣式之前,在您的使用者介面中看不到這些資訊。

設定 Headless UI 組件不同狀態樣式的最簡單方法是使用每個組件公開使用的 data-* 屬性。

例如,ListboxOption 組件公開一個 data-focus 屬性,讓您知道該選項目前是否已使用滑鼠或鍵盤設有焦點,以及一個 data-selected 屬性,讓您知道該選項是否與 Listbox 的目前 value 相符。

<!-- Rendered `ListboxOption` -->
<div data-focus data-selected>Arlene Mccoy</div>

使用 CSS 屬性選取器根據這些資料屬性的存在與否,有條件地套用樣式。如果您使用的是 Tailwind CSS,則 資料屬性修改器 可以讓您輕鬆完成這件事

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
<ListboxOption key={person.id} value={person} className="group flex gap-2 bg-white data-[focus]:bg-blue-100">
<CheckIcon className="invisible size-5 group-data-[selected]:visible" />
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

請參閱 組件 API以獲取可用資料屬性的清單。

每個組件還會透過 渲染屬性 公開其目前狀態的資訊,您可以使用這些屬性來有條件地套用不同的樣式,或者渲染不同的內容。

例如,ListboxOption 組件公開一個 focus 狀態,讓您知道該選項目前是否已使用滑鼠或鍵盤設有焦點,以及一個 selected 狀態,讓您知道該選項是否與 Listbox 的目前 value 相符。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions anchor="bottom">
        {people.map((person) => (
<ListboxOption key={person.id} value={person} as={Fragment}>
{({ focus, selected }) => (
<div className={clsx('flex gap-2', focus && 'bg-blue-100')}>
<CheckIcon className={clsx('size-5', !selected && 'invisible')} />
{person.name}
</div>
)}
</ListboxOption>
))} </ListboxOptions> </Listbox> ) }

請參閱 組件 API以獲取可用渲染屬性的清單。

使用 Field 元件包覆 LabelListbox,以使用產生的 ID 自動關聯他們

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field>
<Label>Assignee:</Label>
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox>
</Field>
) }

Field 元件內使用 Description 元件,以使用 aria-describedby 屬性將其自動與 Listbox 關聯

import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field>
<Label>Assignee:</Label>
<Description>This person will have full access to this project.</Description>
<Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox>
</Field>
) }

新增 disabled 屬性至 Field 元件,以停用 Listbox 及其相關的 LabelDescription

import { Description, Field, Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Field disabled>
<Label>Assignee:</Label> <Description>This person will have full access to this project.</Description> <Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> ) }

您還可以在 Field 元件外停用下拉選單,方法是將 disabled 屬性直接新增至 Listbox 本身。

使用 disabled 屬性來禁用 ListboxOption 並防止它被選取

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

const people = [
  { id: 1, name: 'Durward Reynolds', available: true },
  { id: 2, name: 'Kenton Towne', available: true },
  { id: 3, name: 'Therese Wunsch', available: true },
{ id: 4, name: 'Benedict Kessler', available: false },
{ id: 5, name: 'Katelyn Rohan', available: true }, ] function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return ( <Listbox value={selectedPerson} onChange={setSelectedPerson}> <ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person}
disabled={!person.available}
className="data-[focus]:bg-blue-100 data-[disabled]:opacity-50"
>
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果您在 Listbox 中新增 name 屬性,將會產生一個隱藏的 input 元素,並與下拉選單的狀態保持同步。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<form action="/projects/1" method="post">
<Field> <Label>Assignee:</Label>
<Listbox name="assignee" value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> <button>Submit</button>
</form>
) }

這讓您可以在原生 HTML <form> 內部使用下拉選單,並讓傳統的應用程式提交格式,就像您的下拉選單是一般的原生 HTML 表單控制項一樣。

基本值(如字串)會顯示為包含該值的單一隱藏輸入欄位,但複雜的值(如物件)會使用方括弧符號作為名稱,編碼成多個輸入欄位

<!-- Rendered hidden inputs -->
<input type="hidden" name="assignee[id]" value="1" />
<input type="hidden" name="assignee[name]" value="Durward Reynolds" />

如果您省略 value 屬性,Headless UI 將會為您在內部追蹤其狀態,讓您能將它當成 不受控制元件 使用。

以不受控制的狀態時,請使用 defaultValue 屬性來提供 Listbox 初始值。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  return (
    <form action="/projects/1" method="post">
      <Field>
        <Label>Assignee:</Label>
<Listbox name="assignee" defaultValue={people[0]}>
<ListboxButton>{({ value }) => value.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> </Field> <button>Submit</button> </form> ) }

使用清單方塊時,它可以簡化您的程式碼 搭配 HTML 表單 或使用表單 API,而這些 API 會透過 FormData 來收集狀態,而不是使用 React 狀態來追蹤它。

只要元件的值變更,您提供的任何 onChange 屬性仍會被呼叫,以防您需要執行任何副作用,但您無需使用它自行追蹤元件的狀態。

ListboxOptions 下拉選單預設沒有設定寬度,但您可以使用 CSS 新增。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-52">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果您希望下拉選單寬度與 ListboxButton 寬度相符,請使用 --button-width CSS 變數,而此變數會顯示在 ListboxOptions 元素上。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="w-[var(--button-width)]">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

anchor 屬性加入到 ListboxOptions 以自動定位下拉選單,使其相對於 ListboxButton

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

使用值 toprightbottomleft 來將下拉選單置中於適當的邊緣,或結合 startend 來將下拉選單對齊到特定角,例如 top startbottom end

若要控制按鈕與下拉選單之間的間距,請使用 --anchor-gap CSS 變數。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom start" className="[--anchor-gap:4px] sm:[--anchor-gap:8px]">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

此外,您可以使用 --anchor-offset 來控制下拉選單應從其原始位置推移的距離,以及使用 --anchor-padding 來控制下拉選單與視窗之間應存在的最小空間。

anchor 屬性還支援物件 API,允許您使用 JavaScript 來控制 gapoffsetpadding 值。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor={{ to: 'bottom start', gap: '4px' }}>
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

有關這些選項的更多資訊,請參閱 ListboxOptions API

如果您已為您的 ListboxOptions 設定以橫向顯示,請在 Listbox 元件上使用 horizontal 屬性以使用左右箭頭鍵來導覽選項,而非上下箭頭鍵,並更新輔助技術的 aria-orientation 屬性。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
<Listbox horizontal value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" className="flex flex-row">
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

若要播放清單方塊下拉選單的開啟和關閉動畫,請將 transition 屬性加入到 ListboxOptions 元件中,然後使用 CSS 來設定過場動畫的不同階段。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
      <ListboxButton>{selectedPerson.name}</ListboxButton>
      <ListboxOptions
        anchor="bottom"
transition
className="origin-top transition duration-200 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
>
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

在內部,transition 屬性實作方式與 Transition 元件完全相同。請參閱 Transition 文件 以深入了解。

無頭 UI 也能順利與 React 生態系統中的其他動畫函式庫結合,例如 Framer MotionReact Spring。您只需要公開這些函式庫的某些狀態即可。

例如,使用 Framer Motion 為列表方塊增加動畫效果,請將 static 屬性新增至 ListboxOptions 元件,然後根據 open 渲染屬性條件式渲染列表方塊

import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
{({ open }) => (
<> <ListboxButton>{selectedPerson.name}</ListboxButton> <AnimatePresence>
{open && (
<ListboxOptions
static
as={motion.div} initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} anchor="bottom" className="origin-top" >
{people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions>
)}
</AnimatePresence> </>
)}
</Listbox> ) }

與原生 HTML 表單控制項(僅允許提供字串為值)不同,無頭 UI 也支援繫結複雜的物件。

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

const people = [
{ id: 1, name: 'Durward Reynolds' },
{ id: 2, name: 'Kenton Towne' },
{ id: 3, name: 'Therese Wunsch' },
{ id: 4, name: 'Benedict Kessler' },
{ id: 5, name: 'Katelyn Rohan' },
]
function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton>{selectedPerson.name}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name}
</ListboxOption> ))} </ListboxOptions> </Listbox> ) }

在繫結值為物件時,務必確定您將物件的同一個執行個體用於 Listboxvalue 及對應 ListboxOption,否則兩者會無法相等,造成列表方塊行為異常。

若要讓使用同一物件的不同執行個體變得更輕鬆,您可以使用 by 屬性,依照特定欄位比對物件而非比對物件身分。

當傳遞物件給 value 屬性時,若 by 屬性設定為 id,則預設會比對物件相等性;但您也可以將其設定為您喜歡的任何欄位。

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

const departments = [
  { name: 'Marketing', contact: 'Durward Reynolds' },
  { name: 'HR', contact: 'Kenton Towne' },
  { name: 'Sales', contact: 'Therese Wunsch' },
  { name: 'Finance', contact: 'Benedict Kessler' },
  { name: 'Customer service', contact: 'Katelyn Rohan' },
]

function Example({ selectedDepartment, onChange }) {
  return (
<Listbox value={selectedDepartment} by="name" onChange={onChange}>
<ListboxButton>{selectedDepartment.name}</ListboxButton> <ListboxOptions anchor="bottom"> {departments.map((department) => ( <ListboxOption key={department.name} value={department} className="data-[focus]:bg-blue-100"> {department.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

如果您希望完全控制比對物件的方式,也可以將您自訂的比對函式傳遞給 by 屬性。

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

const departments = [
  { id: 1, name: 'Marketing', contact: 'Durward Reynolds' },
  { id: 2, name: 'HR', contact: 'Kenton Towne' },
  { id: 3, name: 'Sales', contact: 'Therese Wunsch' },
  { id: 4, name: 'Finance', contact: 'Benedict Kessler' },
  { id: 5, name: 'Customer service', contact: 'Katelyn Rohan' },
]

function compareDepartments(a, b) {
return a.name.toLowerCase() === b.name.toLowerCase()
}
function Example({ selectedDepartment, onChange }) { return (
<Listbox value={selectedDepartment} by={compareDepartments} onChange={onChange}>
<ListboxButton>{selectedDepartment.name}</ListboxButton> <ListboxOptions anchor="bottom"> {departments.map((department) => ( <ListboxOption key={department.id} value={department} className="data-[focus]:bg-blue-100"> {department.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

若要允許在您的列表方塊中選取多個值,請使用 multiple 屬性,並將陣列傳遞給 value,而非單一選項。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
const [selectedPeople, setSelectedPeople] = useState([people[0], people[1]])
return (
<Listbox value={selectedPeople} onChange={setSelectedPeople} multiple>
<ListboxButton>{selectedPeople.map((person) => person.name).join(', ')}</ListboxButton> <ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

這會在您選取選項時讓列表方塊保持開啟狀態,而選擇選項將會在適當的位置切換選項。

只要新增或移除選項,您的 onChange 處理函式就會呼叫,並傳入包含所有選取選項的陣列。

預設情況下,Listbox 及其子元件各渲染一個對該元件適用的預設元素。

例如,ListboxButton 會產生一個 buttonListboxOptions 會產生一個 div,而 ListboxOption 會產生 div。另一方面,Listbox不會產生元素,而是直接產生其子元素。

請使用 as 屬性來將元件產生為不同的元素或作為您自己的自訂元件,確保自訂元件會轉送參考,這樣 Headless UI 才能正確連接事物。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

let MyCustomButton = forwardRef(function (props, ref) {
return <button className="..." ref={ref} {...props} />
})
function Example() { const [selectedPerson, setSelectedPerson] = useState(people[0]) return (
<Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={MyCustomButton}>{selectedPerson.name}</ListboxButton>
<ListboxOptions anchor="bottom" as="ul"> {people.map((person) => (
<ListboxOption as="li" key={person.id} value={person} className="data-[focus]:bg-blue-100">
{person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

若要告知元素直接產生其子元素,而不產生包裝器元素,請使用 Fragment

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <Listbox value={selectedPerson} onChange={setSelectedPerson}>
<ListboxButton as={Fragment}>
<button>{selectedPerson.name}</button>
</ListboxButton>
<ListboxOptions anchor="bottom"> {people.map((person) => ( <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100"> {person.name} </ListboxOption> ))} </ListboxOptions> </Listbox> ) }

建立無按鈕 API

雖然建立自訂列表方塊時需要 ListboxButton 元件,但可以設定為預設包含按鈕,因此不需要每次使用列表方塊時都加入按鈕。舉例來說,類似這樣的 API

<MyListbox name="status">
  <MyListboxOption value="active">Active</MyListboxOption>
  <MyListboxOption value="paused">Paused</MyListboxOption>
  <MyListboxOption value="delayed">Delayed</MyListboxOption>
  <MyListboxOption value="canceled">Canceled</MyListboxOption>
</MyListbox>

若要達成此目的,請在您的 ListboxButton 內使用 ListboxSelectedOption 元件來產生目前選取的列表方塊選項。

作品這樣做,您必須將自訂列表方塊的孩子(所有 ListboxOption 實例)同時傳遞給 ListboxOptions(作為它的孩子),以及傳遞給 ListboxSelectedOption(透過 options 屬性)。

然後,若要根據 ListboxOption 是在 ListboxButtonListboxOptions 中產生來進行樣式調整,請使用 selectedOption 產生屬性來有條件套用不同的樣式或產生不同的內容。

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

const people = [
  { id: 1, name: 'Durward Reynolds' },
  { id: 2, name: 'Kenton Towne' },
  { id: 3, name: 'Therese Wunsch' },
  { id: 4, name: 'Benedict Kessler' },
  { id: 5, name: 'Katelyn Rohan' },
]

function Example() {
  const [selectedPerson, setSelectedPerson] = useState(people[0])

  return (
    <MyListbox value={selectedPerson} onChange={setSelectedPerson} placeholder="Select a person&hellip;">
      {people.map((person) => (
        <MyListboxOption key={person.id} value={person}>
          {person.name}
        </MyListboxOption>
      ))}
    </MyListbox>
  )
}

function MyListbox({ placeholder, children, ...props }) {
  return (
    <Listbox {...props}>
      <ListboxButton>
<ListboxSelectedOption options={children} placeholder={<span className="opacity-50">{placeholder}</span>} />
</ListboxButton>
<ListboxOptions anchor="bottom">{children}</ListboxOptions>
</Listbox> ) } function MyListboxOption({ children, ...props }) { return ( <ListboxOption as={Fragment} {...props}>
{({ selectedOption }) => {
return selectedOption ? children : <div className="data-[focus]:bg-blue-100">{children}</div>
}}
</ListboxOption> ) }

ListboxSelectedOption 元件還有一個 placeholder 屬性,當選項尚未選取時,可以使用該屬性來產生 placeholder。

鍵盤互動

指令說明

Space, ArrowDown, or ArrowUpListboxButton 焦點時

開啟列表方塊並聚焦於選取的項目

EnterListboxButton 焦點時,而 Listbox 為關閉狀態

送出父系表單(若存在的話)

Esc當清單方塊開啟時

關閉清單方塊

ArrowDownArrowUp當清單方塊開啟時

聚焦於上一個/下一個未停用的項目

ArrowLeftArrowRight當清單方塊開啟且設定了horizontal

聚焦於上一個/下一個未停用的項目

HomePageUp當清單方塊開啟時

聚焦於第一個未停用的項目

EndPageDown當清單方塊開啟時

聚焦於最後一個未停用的項目

EnterSpace當清單方塊開啟時

選取當前項目

A–Za–z當清單方塊開啟時

聚焦於與鍵盤輸入相符的第一個項目

主要的清單方塊元件。

屬性預設說明
as片段
字串 | 元件

元件或清單方塊應該作為呈現。

invalidfalse
布林值

如果清單方塊無效。

已停用false
布林值

使用此選項停用整個 Listbox 元件和相關子項。

T

已選取的值。

預設值
T

將元件用作不受控元件時的預設值。

透過
keyof T | ((a: T, z: T) => boolean)

使用此選項依特定欄位比較物件,或傳入您自己的比較函式,以完全控制物件的比較方式。

當您將物件傳遞給 value prop 時,預設會將 by 設定成id(如果存在的話)。

onChange
(value: T) => void

選取新選項時要呼叫的函式。

水平false
布林值

如果為 true,ListboxOptions 的方向會是水平,否則會是垂直

多選false
布林值

是否允許選取多個選項。

名稱
字串

在表單中使用清單方塊時使用名稱。

表單
字串

表單 ID,清單方塊屬於該表單。

如果提供 name 但未提供 form,則清單方塊便會將其狀態新增到最近的祖先 form 元素。

資料屬性呈現 prop說明

T

已選取的值。

資料-開啟開啟

布林值

如果清單方塊已開啟。

資料-invalidinvalid

布林值

如果清單方塊無效。

資料-已停用已停用

布林值

如果清單方塊已停用。

Listbox 的按鈕。

屬性預設說明
as按鈕
字串 | 元件

元件或listbox 按鈕應該作為呈現。

資料屬性呈現 prop說明

T

已選取的值。

資料-開啟開啟

布林值

如果清單方塊已開啟。

資料-invalidinvalid

布林值

如果清單方塊無效。

資料-已停用已停用

布林值

如果listbox 按鈕已停用。

資料-焦點焦點

布林值

如果listbox 按鈕已聚焦。

資料-滑鼠游標停留在上面滑鼠游標停留在上面

布林值

如果listbox 按鈕已滑鼠游標停留在上面。

資料-動作中動作中

布林值

如果listbox 按鈕處於動作中或已按下的狀態。

資料-自動對焦自動對焦

布林值

autoFocus prop 是否已設定成 true

呈現目前選取的選項,或是在沒有選取任何選項的情況下呈現佔位符。設計成要成為 ListboxButton 的子項。

屬性預設說明
as片段
字串 | 元件

元件或listbox 已選取選項應該作為呈現。

佔位符
ReactNode

在沒有選取任何選項時要呈現的 React 元素。

選項
ReactNode[]

您的完整 ListboxOption React 元素陣列。ListboxSelectedOption 會過濾這份清單,以尋找並呈現目前選取的選項。

在自訂 Listbox 中直接包圍選項清單的元件。

屬性預設說明
asdiv
字串 | 元件

元件或listbox 選項應該作為呈現。

轉場false
布林值

元素是否應呈現轉場屬性,例如 data-closed data-enterdata-leave

錨點
物件

設定下拉式選單固定在按鈕上的方式。

anchor.to底部
字串

listbox 選項定位在哪裡

使用值 toprightbottomleft 置中 listbox 選項вдоль соответствующего края или комбинируйте его с start или end, чтобы выровнятьlistbox 選項по определённому углу, например top start или bottom end.

Кроме этого, используйте параметр selection, чтобы расположить текущий выделенный вариант непосредственно надlistbox 按鈕.

anchor.gap0
Число | Строка

Пространство междуlistbox 按鈕иlistbox 選項.

также можно контролировать с помощью CSS-переменной --anchor-gap.

anchor.offset0
Число | Строка

Расстояние, на котороеlistbox 選項следует сдвинуть с его изначального положения.

Также можно контролировать с помощью CSS-переменной --anchor-offset.

anchor.padding0
Число | Строка

Минимальное расстояние междуlistbox 選項и окном обзора.

Можно также контролировать с помощью CSS-переменной --anchor-padding.

staticfalse
布林值

Должен ли элемент игнорировать внутренне управляемое состояние открыто/закрыто.

unmounttrue
布林值

Должен ли элемент быть размонтирован или скрыт в зависимости от состояния открыто/закрыто.

portalfalse
布林值

Должен ли элемент отображаться в портале.

Автоматически устанавливается в true, когда установлен параметр anchor.

modaltrue
布林值

Включить ли вспомогательные функции, такие как блокировка прокрутки, фиксация фокуса и превращение других элементов inert.

資料屬性呈現 prop說明
資料-開啟開啟

布林值

如果清單方塊已開啟。

Используется для упаковки каждого элемента внутри Listbox.

屬性預設說明
asdiv
字串 | 元件

元件或listbox option應該作為呈現。

T

Значение варианта.

已停用false
布林值

如果listbox optionотключенодля навигации с клавиатуры и целей ARIA.

資料屬性呈現 prop說明
資料-выбрановыбрано

布林值

如果listbox optionвыбрано.

資料-已停用已停用

布林值

如果listbox option已停用。

資料-焦點焦點

布林值

如果listbox option已聚焦。

資料-selectedOptionselectedOption

布林值

Является ли опция listbox дочерним элементом ListboxSelectedOption.

Если вас интересуют заранее разработанные примеры селектора и выпадающего списка Tailwind CSS с использованием Headless UI, ознакомьтесь с Tailwind UI — коллекцией прекрасно разработанных и профессионально созданных компонентов, созданных нами.

Это отличный способ поддержать нашу работу над такими проектами с открытым исходным кодом и позволит нам улучшать их и поддерживать их надлежащим образом.