リストボックス
リストボックスは、キーボードナビゲーションの堅牢なサポートを備えた、カスタムでアクセス可能なアプリのセレクトメニューを構築するための優れた基盤です。
まず、npm を使って Headless UI をインストールします。
npm install @headlessui/reactリストボックスは、Listbox、ListboxButton、ListboxSelectedOption、ListboxOptions、および ListboxOption のコンポーネントを使用して構築されています。
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 は、どのリストボックスのオプションが現在選択されているか、ポップオーバーが開いているか閉じているか、メニューのどの項目がキーボードで現在フォーカスされているかなど、各コンポーネントに関する多くの状態を追跡します。
ただし、コンポーネントはヘッドレスであり、初期状態では完全にスタイル設定されていないため、各状態に必要なスタイルを自分で指定するまで、UI でこの情報を確認することはできません。
Headless UI コンポーネントのさまざまな状態をスタイル設定する最も簡単な方法は、各コンポーネントが公開する data-* 属性を使用することです。
たとえば、ListboxOption コンポーネントは、オプションが現在マウスまたはキーボードでフォーカスされているかどうかを示す data-focus 属性と、オプションが Listbox の現在の value と一致するかどうかを示す data-selected 属性を公開します。
<!-- 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 状態と、オプションが Listbox の現在の value と一致するかどうかを示す selected 状態を公開します。
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を参照してください。
Label と Listbox を Field コンポーネントでラップして、生成された 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>  )
}
Listbox と関連付けられた Label および Description を無効にするには、disabled プロップを Field コンポーネントに追加します。
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>
  )
}
Listbox 自体に disabled プロップを直接追加して、Field の外部でリストボックスを無効にすることもできます。
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 フォームで使用する場合や、React の状態を使用して追跡するのではなく、FormData を使用して状態を収集するフォーム API で使用する場合に、コードを簡素化できます。
サイドエフェクトを実行する必要がある場合に備えて、コンポーネントの値が変更されたときに、指定した 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 の幅と一致させたい場合は、ListboxOptions 要素で公開されている --button-width 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-[var(--button-width)]">        {people.map((person) => (
          <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
            {person.name}
          </ListboxOption>
        ))}
      </ListboxOptions>
    </Listbox>
  )
}
ListboxButton を基準にしてドロップダウンを自動的に配置するには、anchor プロップを 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 start">        {people.map((person) => (
          <ListboxOption key={person.id} value={person} className="data-[focus]:bg-blue-100">
            {person.name}
          </ListboxOption>
        ))}
      </ListboxOptions>
    </Listbox>
  )
}
ドロップダウンを適切な端に沿って中央に配置するには、top、right、bottom、または left の値を使用するか、start または end と組み合わせて、ドロップダウンを top start や bottom 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 プロップは、JavaScript を使用して gap、offset、および padding の値を制御できるオブジェクト API もサポートしています。
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>
  )
}
リストボックスのドロップダウンの開閉をアニメーション化するには、ListboxOptions コンポーネントに transition プロパティを追加し、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 のドキュメントを参照してください。
Headless UI は、Framer Motion や React Spring のような React エコシステムの他のアニメーションライブラリともうまく連携します。これらのライブラリにいくつかの状態を公開するだけで済みます。
たとえば、Framer Motion でリストボックスをアニメーション化するには、ListboxOptions コンポーネントに static プロパティを追加し、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 フォームコントロールとは異なり、Headless 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>
  )
}
オブジェクトを値としてバインドする場合、Listbox の value と対応する 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 は button をレンダリングし、ListboxOptions は div をレンダリングし、ListboxOption は div をレンダリングします。対照的に、Listbox は _要素をレンダリングせず_、代わりにその子を直接レンダリングします。
コンポーネントを異なる要素として、または独自のカスタムコンポーネントとしてレンダリングするには、as プロパティを使用します。カスタムコンポーネントが forward refs を行い、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>
  )
}
カスタムリストボックスを構築する場合、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 コンポーネントを使用して、現在選択されているリストボックスオプションをレンダリングします。
これを機能させるには、カスタムリストボックスの children(すべての ListboxOption インスタンス)を、ListboxOptions の子として、および options プロパティを介して ListboxSelectedOption の両方に渡す必要があります。
次に、ListboxButton でレンダリングされているか ListboxOptions でレンダリングされているかに基づいて ListboxOption をスタイル設定するには、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…">
      {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 プロパティもあります。
| コマンド | 説明 | 
| Space、下矢印、または上矢印 | リストボックスを開き、選択した項目にフォーカスを合わせます | 
| Enter | 親フォームが存在する場合は、それを送信します | 
| Escリストボックスが開いている場合 | リストボックスを閉じます | 
| 下矢印または上矢印リストボックスが開いている場合 | 前/次の無効ではない項目にフォーカスします | 
| 左矢印または右矢印リストボックスが開いていて、 | 前/次の無効ではない項目にフォーカスします | 
| HomeまたはPageUpリストボックスが開いている場合 | 最初の無効ではない項目にフォーカスします | 
| EndまたはPageDownリストボックスが開いている場合 | 最後の無効ではない項目にフォーカスします | 
| EnterまたはSpaceリストボックスが開いている場合 | 現在の項目を選択します | 
| A–Zまたはa–zリストボックスが開いている場合 | キーボード入力に一致する最初の項目にフォーカスします | 
| プロパティ | デフォルト | 説明 | 
| as | Fragment | 文字列 | コンポーネント要素またはコンポーネントリストボックスとしてレンダリングする必要があります。 | 
| invalid | false | ブール値であるかどうかリストボックスが無効です。 | 
| disabled | false | ブール値これを使用して、 | 
| value | — | T選択した値。 | 
| defaultValue | — | T非制御コンポーネントとして使用する場合のデフォルト値。 | 
| by | — | keyof T | ((a: T, z: T) => boolean)これを使用して、特定のフィールドでオブジェクトを比較するか、オブジェクトの比較方法を完全に制御するために独自の比較関数を渡します。 オブジェクトを  | 
| onChange | — | (value: T) => void新しいオプションが選択されたときに呼び出す関数。 | 
| horizontal | false | ブール値true の場合、 | 
| multiple | false | ブール値複数のオプションを選択できるかどうか。 | 
| name | — | 文字列を使用する場合に使用される名前リストボックスフォーム内。 | 
| form | — | 文字列のフォームの idリストボックスが属します。 
 | 
| データ属性 | レンダリングプロパティ | 説明 | 
| — | value | 
 選択した値。 | 
| data-open | open | 
 であるかどうかリストボックスが開いています。 | 
| data-invalid | invalid | 
 であるかどうかリストボックスが無効です。 | 
| data-disabled | disabled | 
 であるかどうかリストボックスが無効です。 | 
| プロパティ | デフォルト | 説明 | 
| as | ボタン | 文字列 | コンポーネント要素またはコンポーネントリストボックスボタンとしてレンダリングする必要があります。 | 
| データ属性 | レンダリングプロパティ | 説明 | 
| — | value | 
 選択した値。 | 
| data-open | open | 
 であるかどうかリストボックスが開いています。 | 
| data-invalid | invalid | 
 であるかどうかリストボックスが無効です。 | 
| data-disabled | disabled | 
 であるかどうかリストボックスボタンが無効です。 | 
| data-focus | focus | 
 であるかどうかリストボックスボタンがフォーカスされています。 | 
| data-hover | hover | 
 であるかどうかリストボックスボタンがホバーされています。 | 
| data-active | active | 
 であるかどうかリストボックスボタンアクティブまたは押された状態です。 | 
| data-autofocus | autofocus | 
 
 | 
| プロパティ | デフォルト | 説明 | 
| as | Fragment | 文字列 | コンポーネント要素またはコンポーネントリストボックスの選択されたオプションとしてレンダリングする必要があります。 | 
| placeholder | — | ReactNodeオプションが選択されていない場合にレンダリングする React 要素。 | 
| options | — | ReactNode[]
 | 
| プロパティ | デフォルト | 説明 | 
| as | div | 文字列 | コンポーネント要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。 | 
| transition | false | ブール値要素が | 
| anchor | — | オブジェクトドロップダウンをボタンに固定する方法を設定します。 | 
| anchor.to | bottom | 文字列どこに配置するかリストボックスのオプショントリガーを基準にします。 
 または、 | 
| anchor.gap | 0 | 数値 | 文字列の間のスペースリストボックスボタンとリストボックスのオプション. 
 | 
| anchor.offset | 0 | 数値 | 文字列の距離リストボックスのオプション元の位置からずらす必要があります。 
 | 
| anchor.padding | 0 | 数値 | 文字列の間の最小スペースリストボックスのオプションとビューポート。 
 | 
| static | false | ブール値要素が内部で管理されている開閉状態を無視するかどうか。 | 
| unmount | true | ブール値要素を開閉状態に基づいてアンマウントするか、非表示にするか。 | 
| portal | false | ブール値要素をポータルにレンダリングするかどうか。 
 | 
| modal | true | ブール値スクロールロック、フォーカストラップなどのアクセシビリティ機能を有効にするかどうか、他の要素を  | 
| データ属性 | レンダリングプロパティ | 説明 | 
| data-open | open | 
 であるかどうかリストボックスが開いています。 | 
| プロパティ | デフォルト | 説明 | 
| as | div | 文字列 | コンポーネント要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。 | 
| value | — | Tオプション値。 | 
| disabled | false | ブール値であるかどうかリストボックスのオプション無効化されているキーボードナビゲーションとARIAの目的のため. | 
| データ属性 | レンダリングプロパティ | 説明 | 
| data-selected | selected | 
 であるかどうかリストボックスのオプション選択されているかどうか。 | 
| data-disabled | disabled | 
 であるかどうかリストボックスのオプションが無効です。 | 
| data-focus | focus | 
 であるかどうかリストボックスのオプションがフォーカスされています。 | 
| data-selectedOption | selectedOption | 
 リストボックスオプションが | 
Headless UIを使用した、事前に設計されたTailwind CSSセレクトメニューとリストボックスの例に関心がある場合は、Tailwind UIをご覧ください。これは、当社が作成した美しくデザインされ、専門的に作られたコンポーネントのコレクションです。
これは、このようなオープンソースプロジェクトに対する私たちの活動をサポートし、それらを改善し、適切に維持できるようにするための素晴らしい方法です。
