列表框 (选择)

列表框是构建应用程序自定义、无障碍选择菜单的绝佳基础,并提供对键盘导航的强大支持。

要开始使用,请通过 npm 安装无头 UI。

请注意,此库仅支持 Vue 3

npm install @headlessui/vue

列表框是使用 ListboxListboxButtonListboxOptionsListboxOptionListboxLabel 组件构建的。

ListboxButton 会在单击时自动打开/关闭 ListboxOptions,当菜单打开时,项目列表会获得焦点并自动可通过键盘导航。

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" :disabled="person.unavailable" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { id: 1, name: 'Durward Reynolds', unavailable: false }, { id: 2, name: 'Kenton Towne', unavailable: false }, { id: 3, name: 'Therese Wunsch', unavailable: false }, { id: 4, name: 'Benedict Kessler', unavailable: true }, { id: 5, name: 'Katelyn Rohan', unavailable: false }, ] const selectedPerson = ref(people[0]) </script>

无头 UI 会跟踪有关每个组件的许多状态,例如当前选择了哪个列表框选项、弹出框是打开还是关闭,或者列表框中的哪个项目当前通过键盘处于活动状态。

但由于组件是无头的,并且在开箱即用时完全没有样式,因此在您自己提供每个状态所需的样式之前,您无法在 UI 中看到这些信息。

每个组件都通过插槽属性公开其当前状态的信息,您可以使用这些属性有条件地应用不同的样式或呈现不同的内容。

例如,ListboxOption 组件公开了 active 状态,该状态告诉您该项目当前是否通过鼠标或键盘处于活动状态。

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Use the `active` state to conditionally style the active option. --> <!-- Use the `selected` state to conditionally style the selected option. --> <ListboxOption v-for="person in people" :key="person.id" :value="person" as="template"
v-slot="{ active, selected }"
>
<li :class="{
'bg-blue-500 text-white': active,
'bg-white text-black': !active,
}"
>
<CheckIcon v-show="selected" />
{{ person.name }} </li> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/20/solid' 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' }, ] const selectedPerson = ref(people[0]) </script>

有关所有可用插槽属性的完整列表,请参阅组件 API 文档

每个组件还通过 data-headlessui-state 属性公开其当前状态的信息,您可以使用这些属性有条件地应用不同的样式。

插槽属性 API中的任何状态为 true 时,它们将作为以空格分隔的字符串列在这个属性中,以便您可以使用CSS 属性选择器[attr~=value] 的形式对它们进行定位。

例如,以下是当列表框打开且第二个项目处于 active 状态时,带有某些子 ListboxOption 组件的 ListboxOptions 组件的渲染内容

<!-- Rendered `ListboxOptions` --> <ul data-headlessui-state="open"> <li data-headlessui-state="">Wade Cooper</li> <li data-headlessui-state="active selected">Arlene Mccoy</li> <li data-headlessui-state="">Devon Webb</li> </ul>

如果您使用的是Tailwind CSS,您可以使用@headlessui/tailwindcss 插件使用修饰符(如 ui-open:*ui-active:*)来定位此属性

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"
class="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>
<CheckIcon class="hidden ui-selected:block" />
{{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' import { CheckIcon } from '@heroicons/vue/20/solid' 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' }, ] const selectedPerson = ref(people[0]) </script>

与仅允许您提供字符串作为值的本机 HTML 表单控件不同,无头 UI 还支持绑定复杂对象。

<template>
<Listbox v-model="selectedPerson">
<ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id"
:value="person"
:disabled="person.unavailable" >
{{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue'
const people = [
{ id: 1, name: 'Durward Reynolds', unavailable: false },
{ id: 2, name: 'Kenton Towne', unavailable: false },
{ id: 3, name: 'Therese Wunsch', unavailable: false },
{ id: 4, name: 'Benedict Kessler', unavailable: true },
{ id: 5, name: 'Katelyn Rohan', unavailable: false },
]
const selectedPerson = ref(people[1])
</script>

将对象绑定为值时,务必确保您使用同一实例的该对象作为 Listboxvalue 以及相应的 ListboxOption,否则它们将无法相等,并会导致列表框行为异常。

为了更轻松地处理同一对象的多个实例,您可以使用 by 属性按特定字段比较对象,而不是比较对象标识

<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
by="id"
>
<ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue']) 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' }, ] </script>

如果您希望完全控制对象的比较方式,也可以将自己的比较函数传递给 by 属性

<template> <Listbox :modelValue="modelValue" @update:modelValue="value => emit('update:modelValue', value)"
:by="compareDepartments"
>
<ListboxButton>{{ modelValue.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="department in departments" :key="department.id" :value="department" > {{ department.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const props = defineProps({ modelValue: Object }) const emit = defineEmits(['update:modelValue'])
function compareDepartments(a, b) {
return a.name.toLowerCase() === b.name.toLowerCase()
}
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' }, ]
</script>

要允许在列表框中选择多个值,请使用 multiple 属性并将数组传递给 v-model,而不是传递单个选项。

<template>
<Listbox v-model="selectedPeople" multiple>
<ListboxButton> {{ selectedPeople.map((person) => person.name).join(', ') }} </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ]
const selectedPeople = ref([people[0], people[1]])
</script>

这将使列表框在您选择选项时保持打开状态,选择选项将将其切换到位。

每当添加或删除选项时,您的 v-model 绑定将使用包含所有选中选项的数组进行更新。

默认情况下,Listbox 将使用按钮内容作为屏幕阅读器的标签。如果您希望更详细地控制向辅助技术宣布的内容,请使用 ListboxLabel 组件。

<template> <Listbox v-model="selectedPerson">
<ListboxLabel>Assignee:</ListboxLabel>
<ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxLabel, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

如果您将 name 属性添加到列表框中,则会渲染隐藏的 input 元素,并与您选择的 value 保持同步。

<template> <form action="/projects/1/assignee" method="post">
<Listbox v-model="selectedPerson" name="assignee">
<ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

这使您可以在本机 HTML <form> 内使用列表框,并进行传统的表单提交,就好像您的列表框是本机 HTML 表单控件一样。

像字符串这样的基本值将被渲染为包含该值的单个隐藏输入,但像对象这样的复杂值将使用方括号表示法对名称进行编码,从而被编码为多个输入

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

如果您向 Listbox 提供 defaultValue 属性而不是 value 属性,无头 UI 将为您在内部跟踪其状态,使您可以将其用作非受控组件

您可以通过 ListboxListboxButton 组件上的 value 插槽属性访问当前选中的选项。

<template> <form action="/projects/1/assignee" method="post">
<Listbox name="assignee" :defaultValue="people[0]">
<ListboxButton v-slot="{ value }">{{ value.name }}</ListboxButton>
<ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> <button>Submit</button> </form> </template> <script setup> import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] </script>

当与 HTML 表单一起使用或使用通过FormData 收集其状态而不是使用 React 状态来跟踪其状态的表单 API 时,这可以简化您的代码。

您提供的任何 @update:modelValue 属性都将在组件的值更改时被调用(以防您需要运行任何副作用),但您无需使用它来自己跟踪组件的状态。

默认情况下,您的 ListboxOptions 实例将根据 Listbox 组件本身内部跟踪的 open 状态自动显示/隐藏。

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- By default, the `ListboxOptions` will automatically show/hide when the `ListboxButton` is pressed. --> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>

如果您希望自己处理此操作(也许是因为出于某种原因您需要添加额外的包装元素),则可以向 ListboxOptions 实例添加 static 属性以告诉它始终渲染,并检查 Listbox 提供的 open 插槽属性以控制哪个元素自己显示/隐藏。

<template>
<Listbox v-model="selectedPerson" v-slot="{ open }">
<ListboxButton>{{ selectedPerson.name }}</ListboxButton>
<div v-show="open">
<!-- Using the `static` prop, the `ListboxOptions` are always rendered and the `open` state is ignored. -->
<ListboxOptions static>
<ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </div> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds' }, { name: 'Kenton Towne' }, { name: 'Therese Wunsch' }, { name: 'Benedict Kessler' }, { name: 'Katelyn Rohan' }, ] const selectedPerson = ref(people[0]) </script>

使用 disabled 属性禁用 ListboxOption。这将使其无法通过鼠标和键盘选择,并且在按下向上/向下箭头时将被跳过。

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions> <!-- Disabled options will be skipped by keyboard navigation. --> <ListboxOption v-for="person in people" :key="person.name" :value="person"
:disabled="person.unavailable"
>
<span :class='{ "opacity-75": person.unavailable }'> {{ person.name }} </span> </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' const people = [ { name: 'Durward Reynolds', unavailable: true }, { name: 'Kenton Towne', unavailable: false }, { name: 'Therese Wunsch', unavailable: false }, { name: 'Benedict Kessler', unavailable: true }, { name: 'Katelyn Rohan', unavailable: false }, ] const selectedPerson = ref(people[0]) </script>

要为列表框的打开/关闭设置动画,您可以使用 Vue 的内置 <transition> 组件。您所需要做的就是将您的 ListboxOptions 实例包装在一个 <transition> 中,过渡将自动应用。

<template> <Listbox v-model="selectedPerson"> <ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Use Vue's built-in `transition` component to add transitions. -->
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person" > {{ person.name }} </ListboxOption> </ListboxOptions> </transition> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

如果您希望为列表框的不同子项协调多个过渡,请查看无头 UI 中包含的过渡组件

默认情况下,Listbox 及其子组件分别渲染一个适合该组件的默认元素。

例如,ListboxLabel 默认渲染一个 labelListboxButton 渲染一个 buttonListboxOptions 渲染一个 ulListboxOption 渲染一个 li。相比之下,Listbox不会渲染元素,而是直接渲染其子项。

这很容易使用 as 属性更改,该属性存在于每个组件上。

<template> <!-- Render a `div` instead of nothing -->
<Listbox as="div" v-model="selectedPerson">
<ListboxButton>{{ selectedPerson.name }}</ListboxButton> <!-- Render a `div` instead of a `ul` -->
<ListboxOptions as="div">
<!-- Render a `span` instead of a `li` --> <ListboxOption
as="span"
v-for="person in people" :key="person.id" :value="person" >
{{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

要告诉元素直接渲染其子项,而没有包装元素,请使用 as="template"

<template> <Listbox v-model="selectedPerson"> <!-- Render children directly instead of a `ListboxButton` -->
<ListboxButton as="template">
<button>{{ selectedPerson.name }}</button> </ListboxButton> <ListboxOptions> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

如果您已将 ListboxOptions 设置为水平显示,请在 Listbox 组件上使用 horizontal 属性以启用使用左右箭头键而不是上下箭头键导航项目,并更新辅助技术的 aria-orientation 属性。

<template>
<Listbox v-model="selectedPerson" horizontal>
<ListboxButton>{{ selectedPerson.name }}</ListboxButton> <ListboxOptions class="flex flex-row"> <ListboxOption v-for="person in people" :key="person.id" :value="person"> {{ person.name }} </ListboxOption> </ListboxOptions> </Listbox> </template> <script setup> import { ref } from 'vue' import { Listbox, ListboxButton, ListboxOptions, ListboxOption, } from '@headlessui/vue' 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' }, ] const selectedPerson = ref(people[0]) </script>

当列表框切换打开时,ListboxOptions 会获得焦点。焦点会一直停留在项目列表中,直到按下Escape或用户单击选项之外的区域。关闭列表框会将焦点返回到 ListboxButton

单击 ListboxButton 会切换选项列表的打开和关闭。单击选项列表之外的任何位置都会关闭列表框。

命令描述

Enter, 空格, 向下箭头, 或者 向上箭头ListboxButton 获得焦点时

打开列表框并使选定项获得焦点

Esc 当列表框打开时

关闭列表框

向下箭头 或者 向上箭头当列表框打开时

使前一个/下一个非禁用项获得焦点

向左箭头 或者 向右箭头当列表框打开且 horizontal 设置为 true 时

使前一个/下一个非禁用项获得焦点

Home 或者 PageUp 当列表框打开时

使第一个非禁用项获得焦点

End 或者 PageDown 当列表框打开时

使最后一个非禁用项获得焦点

Enter 或者 空格 当列表框打开时

选择当前项

A–Z 或者 a–z 当列表框打开时

使与键盘输入匹配的第一个项获得焦点

所有相关的 ARIA 属性都将自动管理。

主要的列表框组件。

属性默认值描述
astemplate
String | Component

Listbox 应该渲染的元素或组件。

v-model
T

选定的值。

defaultValue
T

使用无控制组件时的默认值。

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

使用此属性可以通过特定字段比较对象,或传递您自己的比较函数来完全控制对象比较方式。

disabledfalse
Boolean

使用此属性可禁用整个列表框组件及其相关子组件。

horizontalfalse
Boolean

如果为 true,则 ListboxOptions 的方向为 horizontal,否则为 vertical

name
String

在表单中使用此组件时的名称。

multiplefalse
Boolean

是否可以选中多个选项。

插槽属性描述
value

T

选定的值。

open

Boolean

列表框是否打开。

disabled

Boolean

列表框是否禁用。

列表框的按钮。

属性默认值描述
asbutton
String | Component

ListboxButton 应该渲染的元素或组件。

插槽属性描述
value

T

选定的值。

open

Boolean

列表框是否打开。

disabled

Boolean

列表框是否禁用。

可用于更精确地控制列表框向屏幕阅读器播报的文本的标签。它的 id 属性将自动生成,并通过 aria-labelledby 属性链接到根 Listbox 组件。

属性默认值描述
aslabel
String | Component

ListboxLabel 应该渲染的元素或组件。

插槽属性描述
open

Boolean

列表框是否打开。

disabled

Boolean

列表框是否禁用。

直接包装自定义列表框中选项列表的组件。

属性默认值描述
asul
String | Component

ListboxOptions 应该渲染的元素或组件。

staticfalse
Boolean

元素是否应该忽略内部管理的打开/关闭状态。

unmounttrue
Boolean

元素是否应该根据打开/关闭状态卸载或隐藏。

插槽属性描述
open

Boolean

列表框是否打开。

用于包装列表框中的每个项。

属性默认值描述
value
T

选项值。

asli
String | Component

ListboxOption 应该渲染的元素或组件。

disabledfalse
Boolean

选项是否应该针对键盘导航和 ARIA 处于禁用状态。

插槽属性描述
active

Boolean

选项是否为活动/焦点选项。

selected

Boolean

选项是否为选中选项。

disabled

Boolean

选项是否针对键盘导航和 ARIA 处于禁用状态。

如果您对使用 Headless UI 和 Tailwind CSS 的预先设计的组件示例感兴趣,请查看 Tailwind UI - 我们制作的一系列设计精美、精心制作的组件。

这是支持我们参与此类开源项目工作的一个好方法,它使我们能够改进这些项目并确保它们得到良好的维护。