リストボックス

リストボックスは、キーボードナビゲーションの堅牢なサポートを備えた、カスタムでアクセス可能なアプリのセレクトメニューを構築するための優れた基盤です。

まず、npm を使って Headless UI をインストールします。

npm install @headlessui/react

リストボックスは、ListboxListboxButtonListboxSelectedOptionListboxOptions、および 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を参照してください。

LabelListboxField コンポーネントでラップして、生成された 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> ) }

Listboxname プロップを追加すると、非表示の 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> ) }

ドロップダウンを適切な端に沿って中央に配置するには、toprightbottom、または left の値を使用するか、start または end と組み合わせて、ドロップダウンを 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 プロップは、JavaScript を使用して gapoffset、および 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 MotionReact 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> ) }

オブジェクトを値としてバインドする場合、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 とそのサブコンポーネントはそれぞれ、そのコンポーネントに適したデフォルトの要素をレンダリングします。

たとえば、ListboxButtonbutton をレンダリングし、ListboxOptionsdiv をレンダリングし、ListboxOptiondiv をレンダリングします。対照的に、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&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 プロパティもあります。

コマンド説明

Space下矢印または上矢印ListboxButton にフォーカスがある場合

リストボックスを開き、選択した項目にフォーカスを合わせます

EnterListboxButton にフォーカスがあり、Listbox が閉じている場合

親フォームが存在する場合は、それを送信します

Escリストボックスが開いている場合

リストボックスを閉じます

下矢印または上矢印リストボックスが開いている場合

前/次の無効ではない項目にフォーカスします

左矢印または右矢印リストボックスが開いていて、horizontal が設定されている場合

前/次の無効ではない項目にフォーカスします

HomeまたはPageUpリストボックスが開いている場合

最初の無効ではない項目にフォーカスします

EndまたはPageDownリストボックスが開いている場合

最後の無効ではない項目にフォーカスします

EnterまたはSpaceリストボックスが開いている場合

現在の項目を選択します

A–Zまたはa–zリストボックスが開いている場合

キーボード入力に一致する最初の項目にフォーカスします

メインのリストボックスコンポーネント。

プロパティデフォルト説明
asFragment
文字列 | コンポーネント

要素またはコンポーネントリストボックスとしてレンダリングする必要があります。

invalidfalse
ブール値

であるかどうかリストボックスが無効です。

disabledfalse
ブール値

これを使用して、Listbox コンポーネント全体と関連する子を無効にします。

value
T

選択した値。

defaultValue
T

非制御コンポーネントとして使用する場合のデフォルト値。

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

これを使用して、特定のフィールドでオブジェクトを比較するか、オブジェクトの比較方法を完全に制御するために独自の比較関数を渡します。

オブジェクトを value プロパティに渡すと、by は存在する場合はデフォルトでidになります。

onChange
(value: T) => void

新しいオプションが選択されたときに呼び出す関数。

horizontalfalse
ブール値

true の場合、ListboxOptions の向きは horizontal になり、それ以外の場合は vertical になります。

multiplefalse
ブール値

複数のオプションを選択できるかどうか。

name
文字列

を使用する場合に使用される名前リストボックスフォーム内。

form
文字列

のフォームの idリストボックスが属します。

name が指定されているが form が指定されていない場合、リストボックスは、その状態を最も近い先祖の form 要素に追加します。

データ属性レンダリングプロパティ説明
value

T

選択した値。

data-openopen

ブール値

であるかどうかリストボックスが開いています。

data-invalidinvalid

ブール値

であるかどうかリストボックスが無効です。

data-disableddisabled

ブール値

であるかどうかリストボックスが無効です。

リストボックスのボタン。

プロパティデフォルト説明
asボタン
文字列 | コンポーネント

要素またはコンポーネントリストボックスボタンとしてレンダリングする必要があります。

データ属性レンダリングプロパティ説明
value

T

選択した値。

data-openopen

ブール値

であるかどうかリストボックスが開いています。

data-invalidinvalid

ブール値

であるかどうかリストボックスが無効です。

data-disableddisabled

ブール値

であるかどうかリストボックスボタンが無効です。

data-focusfocus

ブール値

であるかどうかリストボックスボタンがフォーカスされています。

data-hoverhover

ブール値

であるかどうかリストボックスボタンがホバーされています。

data-activeactive

ブール値

であるかどうかリストボックスボタンアクティブまたは押された状態です。

data-autofocusautofocus

ブール値

autoFocus プロパティが true に設定されたかどうか。

現在選択されているオプションをレンダリングするか、オプションが選択されていない場合はプレースホルダーをレンダリングします。ListboxButton の子になるように設計されています。

プロパティデフォルト説明
asFragment
文字列 | コンポーネント

要素またはコンポーネントリストボックスの選択されたオプションとしてレンダリングする必要があります。

placeholder
ReactNode

オプションが選択されていない場合にレンダリングする React 要素。

options
ReactNode[]

ListboxOption React 要素の完全な配列。ListboxSelectedOption は、このリストをフィルター処理して、現在選択されているオプションを見つけてレンダリングします。

カスタムListbox内のオプションリストを直接ラップするコンポーネント。

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。

transitionfalse
ブール値

要素がdata-closedのようなトランジション属性をレンダリングするかどうか。 data-enterおよびdata-leave

anchor
オブジェクト

ドロップダウンをボタンに固定する方法を設定します。

anchor.tobottom
文字列

どこに配置するかリストボックスのオプショントリガーを基準にします。

toprightbottomleftの値を使用して、適切なエッジに沿って リストボックスのオプションを中央に配置するか、startまたはendと組み合わせてリストボックスのオプションを特定の角(top startbottom endなど)に合わせます。

または、selectionオプションを使用して、現在選択されているオプションをリストボックスボタン.

anchor.gap0
数値 | 文字列

の間のスペースリストボックスボタンリストボックスのオプション.

--anchor-gap CSS変数を使用して制御することもできます。

anchor.offset0
数値 | 文字列

の距離リストボックスのオプション元の位置からずらす必要があります。

--anchor-offset CSS変数を使用して制御することもできます。

anchor.padding0
数値 | 文字列

の間の最小スペースリストボックスのオプションとビューポート。

--anchor-padding CSS変数を使用して制御することもできます。

staticfalse
ブール値

要素が内部で管理されている開閉状態を無視するかどうか。

unmounttrue
ブール値

要素を開閉状態に基づいてアンマウントするか、非表示にするか。

portalfalse
ブール値

要素をポータルにレンダリングするかどうか。

anchorプロパティが設定されている場合、自動的にtrueに設定されます。

modaltrue
ブール値

スクロールロック、フォーカストラップなどのアクセシビリティ機能を有効にするかどうか、他の要素を inertにするかどうか.

データ属性レンダリングプロパティ説明
data-openopen

ブール値

であるかどうかリストボックスが開いています。

Listbox内の各アイテムをラップするために使用されます。

プロパティデフォルト説明
asdiv
文字列 | コンポーネント

要素またはコンポーネントリストボックスのオプションとしてレンダリングする必要があります。

value
T

オプション値。

disabledfalse
ブール値

であるかどうかリストボックスのオプション無効化されているキーボードナビゲーションとARIAの目的のため.

データ属性レンダリングプロパティ説明
data-selectedselected

ブール値

であるかどうかリストボックスのオプション選択されているかどうか。

data-disableddisabled

ブール値

であるかどうかリストボックスのオプションが無効です。

data-focusfocus

ブール値

であるかどうかリストボックスのオプションがフォーカスされています。

data-selectedOptionselectedOption

ブール値

リストボックスオプションがListboxSelectedOptionの子であるかどうか。

Headless UIを使用した、事前に設計されたTailwind CSSセレクトメニューとリストボックスの例に関心がある場合はTailwind UIをご覧ください。これは、当社が作成した美しくデザインされ、専門的に作られたコンポーネントのコレクションです。

これは、このようなオープンソースプロジェクトに対する私たちの活動をサポートし、それらを改善し、適切に維持できるようにするための素晴らしい方法です。