单选按钮组

单选按钮组为您提供与原生 HTML 单选按钮输入相同的功能,但没有任何样式。它们非常适合构建自定义选择器 UI。

要开始使用,请通过 npm 安装 Headless UI

npm install @headlessui/react

单选按钮组是使用 RadioGroupRadioGroup.LabelRadioGroup.Option 组件构建的。

单击选项将选择它,当单选按钮组获得焦点时,箭头键将更改所选选项。

import { useState } from 'react' import { RadioGroup } from '@headlessui/react' function MyRadioGroup() { let [plan, setPlan] = useState('startup') return ( <RadioGroup value={plan} onChange={setPlan}> <RadioGroup.Label>Plan</RadioGroup.Label> <RadioGroup.Option value="startup"> {({ checked }) => ( <span className={checked ? 'bg-blue-200' : ''}>Startup</span> )} </RadioGroup.Option> <RadioGroup.Option value="business"> {({ checked }) => ( <span className={checked ? 'bg-blue-200' : ''}>Business</span> )} </RadioGroup.Option> <RadioGroup.Option value="enterprise"> {({ checked }) => ( <span className={checked ? 'bg-blue-200' : ''}>Enterprise</span> )} </RadioGroup.Option> </RadioGroup> ) }

Headless UI 会跟踪有关每个组件的许多状态,例如当前选中哪个单选按钮组选项、弹出窗口是打开还是关闭,或者菜单中哪个项目当前通过键盘处于活动状态。

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

每个组件都通过 渲染道具 公开其当前状态的信息,您可以使用这些道具来有条件地应用不同的样式或渲染不同的内容。

例如,RadioGroup.Option 组件公开了一个 active 状态,它告诉您该选项是否通过鼠标或键盘获得焦点,以及一个 checked 状态,它告诉您该选项是否与 RadioGroup 的当前 value 相匹配。

import { useState, Fragment } from 'react' import { RadioGroup } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/20/solid' const plans = ['Statup', 'Business', 'Enterprise'] function MyRadioGroup() { const [plan, setPlan] = useState(plans[0]) return ( <RadioGroup value={plan} onChange={setPlan}> <RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( /* Use the `active` state to conditionally style the active option. */ /* Use the `checked` state to conditionally style the checked option. */ <RadioGroup.Option key={plan} value={plan} as={Fragment}>
{({ active, checked }) => (
<li className={`${
active ? 'bg-blue-500 text-white' : 'bg-white text-black'
}
`
}
>
{checked && <CheckIcon />}
{plan} </li> )} </RadioGroup.Option> ))} </RadioGroup> ) }

有关每个组件的完整渲染道具 API,请参阅 组件 API 文档

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

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

例如,以下是如何在单选按钮组打开并且第二个选项同时为 checkedactive 时,RadioGroup 组件及其一些子 RadioGroup.Option 组件的渲染结果。

<!-- Rendered `RadioGroup` --> <div role="radiogroup"> <li data-headlessui-state="">Statup</li> <li data-headlessui-state="active checked">Business</li> <li data-headlessui-state="">Enterprise</li> </div>

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

import { useState, Fragment } from 'react' import { RadioGroup } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/20/solid' const plans = ['Statup', 'Business', 'Enterprise'] function MyRadioGroup() { const [plan, setPlan] = useState(plans[0]) return ( <RadioGroup value={plan} onChange={setPlan}> <RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( <RadioGroup.Option key={plan} value={plan}
className="ui-active:bg-blue-500 ui-active:text-white ui-not-active:bg-white ui-not-active:text-black"
>
<CheckIcon className="hidden ui-checked:block" />
{plan} </RadioGroup.Option> ))} </RadioGroup> ) }

与只能提供字符串作为值的原生 HTML 表单控件不同,Headless UI 还支持绑定复杂对象。

import { useState } from 'react' import { RadioGroup } from '@headlessui/react'
const plans = [
{ id: 1, name: 'Startup' },
{ id: 2, name: 'Business' },
{ id: 3, name: 'Enterprise' },
]
function MyRadioGroup() { const [plan, setPlan] = useState(plans[0]) return (
<RadioGroup value={plan} onChange={setPlan}>
<RadioGroup.Label>Plan:</RadioGroup.Label> {plans.map((plan) => (
<RadioGroup.Option key={plan.id} value={plan}>
{plan.name} </RadioGroup.Option> ))} </RadioGroup> ) }

当将对象绑定为值时,务必确保使用相同实例的对象作为 RadioGroupvalue 以及相应的 RadioGroup.Option,否则它们将无法相等,会导致单选按钮组行为异常。

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

import { RadioGroup } from '@headlessui/react' const plans = [ { id: 1, name: 'Startup' }, { id: 2, name: 'Business' }, { id: 3, name: 'Enterprise' }, ]
function PlanPicker({ checkedPlan, onChange }) {
return (
<RadioGroup value={checkedPlan} by="id" onChange={onChange}>
<RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( <RadioGroup.Option key={plan.id} value={plan}> {plan.name} </RadioGroup.Option> ))} </RadioGroup> ) }

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

import { RadioGroup } from '@headlessui/react' const plans = [ { id: 1, name: 'Startup' }, { id: 2, name: 'Business' }, { id: 3, name: 'Enterprise' }, ]
function comparePlans(a, b) {
return a.name.toLowerCase() === b.name.toLowerCase()
}
function PlanPicker({ checkedPlan, onChange }) { return (
<RadioGroup value={checkedPlan} by={comparePlans} onChange={onChange}>
<RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( <RadioGroup.Option key={plan.id} value={plan}> {plan.name} </RadioGroup.Option> ))} </RadioGroup> ) }

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

import { useState } from 'react' import { RadioGroup } from '@headlessui/react' const plans = ['startup', 'business', 'enterprise'] function Example() { const [plan, setPlan] = useState(plans[0]) return ( <form action="/billing" method="post">
<RadioGroup value={plan} onChange={setPlan} name="plan">
<RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( <RadioGroup.Option key={plan} value={plan}> {plan} </RadioGroup.Option> ))} </RadioGroup> <button>Submit</button> </form> ) }

这使您可以在原生 HTML <form> 中使用单选按钮组,并进行传统的表单提交,就像您的单选按钮组是原生 HTML 表单控件一样。

字符串等基本值将被渲染为单个隐藏输入,其中包含该值,但对象等复杂值将使用方括号表示法对名称进行编码,并被编码为多个输入。

<input type="hidden" name="plan" value="startup" />

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

import { RadioGroup } from '@headlessui/react' const plans = [ { id: 1, name: 'Startup' }, { id: 2, name: 'Business' }, { id: 3, name: 'Enterprise' }, ] function Example() { return ( <form action="/companies" method="post">
<RadioGroup name="plan" defaultValue={plans[0]}>
<RadioGroup.Label>Plan</RadioGroup.Label> {plans.map((plan) => ( <RadioGroup.Option key={plan.id} value={plan}> {plan.name} </RadioGroup.Option> ))} </RadioGroup> <button>Submit</button> </form> ) }

当将组合框 与 HTML 表单 或使用 FormData 收集其状态而不是使用 React 状态的表单 API 一起使用时,这可以简化您的代码。

您提供的任何 onChange 属性仍然会在组件的值发生变化时被调用,以防您需要运行任何副作用,但您无需使用它来自己跟踪组件的状态。

您可以使用 RadioGroup.LabelRadioGroup.Description 组件来标记每个选项的内容。这样做会自动通过 aria-labelledbyaria-describedby 属性以及自动生成的 id 将每个组件链接到其祖先 RadioGroup.Option 组件,从而提高您自定义选择器的语义和辅助功能。

默认情况下,RatioGroup.Label 渲染一个 label 元素,RadioGroup.Description 渲染一个 <div>。这些也可以使用 as 属性进行自定义,如以下 API 文档中所述。

请注意,LabelDescription 可以嵌套。每个都将引用其最近的祖先组件,无论该祖先是 RadioGroup.Option 还是根 RadioGroup 本身。

import { useState } from 'react' import { RadioGroup } from '@headlessui/react' function MyRadioGroup() { const [selected, setSelected] = useState('startup') return ( <RadioGroup value={selected} onChange={setSelected}> {/* This label is for the root `RadioGroup`. */}
<RadioGroup.Label className="sr-only">Plan</RadioGroup.Label>
<div className="rounded-md bg-white"> <RadioGroup.Option value="startup" className={({ checked }) => ` ${checked ? 'border-indigo-200 bg-indigo-50' : 'border-gray-200'} relative flex border p-4 `} > {({ checked }) => ( <div className="flex flex-col"> {/* This label is for the `RadioGroup.Option`. */}
<RadioGroup.Label
as="span"
className={`${
checked ? 'text-indigo-900' : 'text-gray-900'
} block text-sm font-medium`}
>
Startup
</RadioGroup.Label>
{/* This description is for the `RadioGroup.Option`. */}
<RadioGroup.Description
as="span"
className={`${
checked ? 'text-indigo-700' : 'text-gray-500'
} block text-sm`}
>
Up to 5 active job postings
</RadioGroup.Description>
</div> )} </RadioGroup.Option> </div> </RadioGroup> ) }

单击 RadioGroup.Option 将选择它。

RadioGroup 组件获得焦点时,所有交互都适用。

命令描述

向下箭头向上箭头向左箭头向右箭头

循环遍历单选按钮组的选项

空格键 当尚未选择任何选项时

选择第一个选项

回车键 当在表单中时

提交表单

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

主要的单选按钮组组件。

属性默认值描述
asdiv
String | Component

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

value
T | undefined

RadioGroup 中当前选中的值。

defaultValue
T

作为非受控组件使用时的默认值。

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

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

onChange
() => void

用于更新 RadioGroup 值的函数。

disabledfalse
boolean

RadioGroup 及其所有 RadioGroup.Option 是否被禁用。

name
String

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

每个可选择选项的包装组件。

属性默认值描述
asdiv
String | Component

RadioGroup.Option 应该渲染的元素或组件。

value
T | undefined

当前 RadioGroup.Option 的值。类型应与 RadioGroup 组件中 value 的类型匹配。

disabledfalse
boolean

RadioGroup.Option 是否被禁用。

渲染属性描述
active

布尔值

选项是否处于活动状态(使用鼠标或键盘)。

checked

布尔值

当前选项是否为选中值。

disabled

boolean

当前选项是否被禁用。

渲染一个元素,其 id 属性会自动生成,并通过 aria-labelledby 属性链接到其最近的祖先 RadioGroupRadioGroup.Option 组件。

属性默认值描述
aslabel
String | Component

RadioGroup.Label 应该渲染的元素或组件。

渲染一个元素,其 id 属性会自动生成,并通过 aria-describedby 属性链接到其最近的祖先 RadioGroupRadioGroup.Option 组件。

属性默认值描述
asdiv
String | Component

RadioGroup.Description 应该渲染的元素或组件。

如果您有兴趣使用 Headless UI 和 Tailwind CSS 的预设计组件示例,请查看 Tailwind UI — 由我们构建的包含精美设计和精心制作的组件的集合。

这是支持我们对类似这样的开源项目工作的绝佳方式,使我们能够改进这些项目并保持其良好维护。