#单选组
单选组用于呈现一小组互斥选项。适合所有选项都能在当前界面直接理解,并且用户需要选择一个值才能继续或完成当前设置的场景。
npm install @a3s-lab/ui@0.4.1<fieldset class="fieldset">
<legend data-variant="label">视图密度</legend>
<div class="grid gap-3">
<div class="field" data-orientation="horizontal">
<input
class="input"
type="radio"
id="density-default"
name="density"
value="default"
/>
<label for="density-default">默认</label>
</div>
<div class="field" data-orientation="horizontal">
<input
class="input"
type="radio"
id="density-compact"
name="density"
value="compact"
checked
/>
<label for="density-compact">紧凑</label>
</div>
</div>
</fieldset>import "@a3s-lab/ui/a3s.css";
import "@a3s-lab/ui/all";#用法
导入 Tailwind 和完整的 A3S 样式包:
@import "tailwindcss";
@import "@a3s-lab/ui/a3s.css";为相关的原生单选按钮设置相同的 name,为每个按钮关联可见标签,再通过 fieldset 与 legend,或 role="radiogroup" 与 aria-labelledby,为整组选择提供一个无障碍名称。
<fieldset class="fieldset">
<legend data-variant="label">视图密度</legend>
<div class="grid gap-3">
<div class="field" data-orientation="horizontal">
<input
class="input"
type="radio"
id="density-default"
name="density"
value="default"
/>
<label for="density-default">默认</label>
</div>
<div class="field" data-orientation="horizontal">
<input
class="input"
type="radio"
id="density-compact"
name="density"
value="compact"
checked
/>
<label for="density-compact">紧凑</label>
</div>
</div>
</fieldset>如需按组件加载,请依次导入基础样式、Radio 结构、Field 结构与 A3S 视觉样式。CDN 等其他接入方式参见安装。
@import "@a3s-lab/ui/base.css";
@import "@a3s-lab/ui/components/radio.css";
@import "@a3s-lab/ui/components/field.css";
@import "@a3s-lab/ui/styles/a3s.css";#参数
| 参数 | 值 | 用途 |
|---|---|---|
| 分组 | <fieldset> 或 role="radiogroup" | 为相关选项建立一个有名称的语义边界 |
| 根类名 | radio-group | 应用稳定的分组布局和框架适配器契约 |
| 选项根元素 | <input type="radio"> | 保留原生表单、校验、键盘与无障碍行为 |
class | input | 应用 Radio 结构和视觉样式 |
name | 相同字符串 | 让选项互斥并只提交一个值 |
value | 稳定字符串 | 提供提交给应用的值,不要根据显示文案临时生成 |
checked | 布尔属性 | 标记真实的初始选择;必须由用户选择时应省略 |
disabled | 布尔属性 | 阻止选择,并需要解释该项不可用的原因 |
required | 布尔属性 | 要求同名原生单选组在提交前至少选择一项 |
aria-invalid | true | 在完成校验后公开失败状态 |
aria-describedby | 元素 ID | 关联约束、选择后果或恢复说明 |
#状态与变体
未选中与已选中是正常交互状态。焦点进入原生单选组后,方向键在组选项间移动并同步选择;Tab 只把整组作为一个进出位置。只有当相邻内容解释约束或恢复方式时,禁用态和错误态才有意义。
#带说明
标签说明选项是什么,辅助文案解释选择后的影响。说明文字放在标签之外,并通过 aria-describedby 关联,避免辅助技术把长段说明反复读作选项名称。
适合日常工作的均衡间距。
扩大点击目标并增加行间分隔。
以更紧密的视觉间距显示更多行。
<div
role="radiogroup"
aria-labelledby="density-description-label-zh"
class="radio-group w-fit"
>
<span id="density-description-label-zh" class="label">
视图密度
</span>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="desc-r1"
name="radio-description"
value="default"
class="input"
aria-describedby="desc-r1-copy"
/>
<section>
<label for="desc-r1">默认</label>
<p id="desc-r1-copy">适合日常工作的均衡间距。</p>
</section>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="desc-r2"
name="radio-description"
value="comfortable"
class="input"
aria-describedby="desc-r2-copy"
defaultChecked
/>
<section>
<label for="desc-r2">宽松</label>
<p id="desc-r2-copy">扩大点击目标并增加行间分隔。</p>
</section>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="desc-r3"
name="radio-description"
value="compact"
class="input"
aria-describedby="desc-r3-copy"
/>
<section>
<label for="desc-r3">紧凑</label>
<p id="desc-r3-copy">以更紧密的视觉间距显示更多行。</p>
</section>
</div>
</div>#卡片式选项
只有当每个选项都需要标题与后果说明时才使用卡片。data-variant="choice-card" 会把可见选项标签扩展到完整字段,让整张卡片仍然只是一个原生单选目标,同时避免无效的嵌套标记或第二层分组角色。关联的 <label> 提供无障碍名称,aria-describedby 只补充选择后果,避免辅助技术把说明重复读作名称。
适合个人和小型团队。
增加团队控制与共享审计记录。
增加组织策略与托管访问控制。
<div
role="radiogroup"
aria-labelledby="plan-label-zh"
class="radio-group w-full max-w-md"
>
<span id="plan-label-zh" class="label">
工作区方案
</span>
<div class="field" data-orientation="horizontal" data-variant="choice-card">
<section>
<label for="plus-plan">Plus</label>
<p id="plus-plan-description">适合个人和小型团队。</p>
</section>
<input
type="radio"
id="plus-plan"
name="choice-plan"
value="plus"
class="input"
aria-describedby="plus-plan-description"
defaultChecked
/>
</div>
<div class="field" data-orientation="horizontal" data-variant="choice-card">
<section>
<label for="pro-plan">Pro</label>
<p id="pro-plan-description">增加团队控制与共享审计记录。</p>
</section>
<input
type="radio"
id="pro-plan"
name="choice-plan"
value="pro"
class="input"
aria-describedby="pro-plan-description"
/>
</div>
<div class="field" data-orientation="horizontal" data-variant="choice-card">
<section>
<label for="enterprise-plan">Enterprise</label>
<p id="enterprise-plan-description">增加组织策略与托管访问控制。</p>
</section>
<input
type="radio"
id="enterprise-plan"
name="choice-plan"
value="enterprise"
class="input"
aria-describedby="enterprise-plan-description"
/>
</div>
</div>#字段组
表单问题优先使用 fieldset 与 legend。图例已经提供分组名称,不要再嵌套一个名称相同的 radiogroup。
<fieldset class="fieldset">
<legend data-variant="label">订阅方案</legend>
<p id="subscription-help-zh">按年付费可节省两个月的费用。</p>
<div class="grid gap-3">
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="plan-monthly"
name="subscription-plan"
value="monthly"
class="input"
aria-describedby="subscription-help-zh"
defaultChecked
/>
<label for="plan-monthly">按月($9.99/月)</label>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="plan-yearly"
name="subscription-plan"
value="yearly"
class="input"
aria-describedby="subscription-help-zh"
/>
<label for="plan-yearly">按年($99.99/年)</label>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="plan-lifetime"
name="subscription-plan"
value="lifetime"
class="input"
aria-describedby="subscription-help-zh"
/>
<label for="plan-lifetime">终身($299.99)</label>
</div>
</div>
</fieldset>#禁用选项
只禁用当前不可用的选项,保留其他选择,并把不可用原因放在禁用控件旁边。
当前未连接兼容设备。
<div
role="radiogroup"
aria-labelledby="availability-label-zh"
class="radio-group w-fit"
>
<span id="availability-label-zh" class="label">
执行环境
</span>
<div class="field" data-orientation="horizontal" data-disabled>
<input
type="radio"
id="disabled-1"
name="radio-disabled"
value="accelerated"
class="input"
aria-describedby="disabled-1-reason"
disabled
/>
<section>
<label for="disabled-1">加速环境</label>
<p id="disabled-1-reason">当前未连接兼容设备。</p>
</section>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="disabled-2"
name="radio-disabled"
value="standard"
class="input"
defaultChecked
/>
<label for="disabled-2">标准环境</label>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="disabled-3"
name="radio-disabled"
value="remote"
class="input"
/>
<label for="disabled-3">远程环境</label>
</div>
</div>#无效
不要把已经合法预选的值标记为错误。初始状态不显示错误,只在用户尝试继续后校验;如果仍未选择,应把焦点移到第一个选项,并在用户完成选择或重置表单后清除过期的错误语义。
<form
class="grid gap-4"
data-radio-group-validation-demo="zh"
novalidate
onsubmit="event.preventDefault(); const group = this.querySelector('fieldset'); const options = [...group.querySelectorAll('input[type=radio]')]; if (options.some((option) => option.checked)) return; group.setAttribute('data-invalid', ''); options.forEach((option) => option.setAttribute('aria-invalid', 'true')); group.querySelector('[role=alert]').hidden = false; options[0].focus();"
onreset="const group = this.querySelector('fieldset'); group.removeAttribute('data-invalid'); group.querySelectorAll('[aria-invalid=true]').forEach((option) => option.removeAttribute('aria-invalid')); group.querySelector('[role=alert]').hidden = true;"
>
<fieldset
class="fieldset"
onchange="if (!event.target.matches('input[type=radio]')) return; this.removeAttribute('data-invalid'); this.querySelectorAll('[aria-invalid=true]').forEach((option) => option.removeAttribute('aria-invalid')); this.querySelector('[role=alert]').hidden = true;"
>
<legend data-variant="label">通知方式</legend>
<p
id="notification-preference-error-zh"
class="text-destructive"
role="alert"
hidden
>
请先选择一种接收方式,然后再继续。
</p>
<div class="grid gap-3">
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="invalid-email"
name="notification-preference"
value="email"
class="input"
required
aria-describedby="notification-preference-error-zh"
/>
<label for="invalid-email">仅邮件</label>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="invalid-sms"
name="notification-preference"
value="sms"
class="input"
required
aria-describedby="notification-preference-error-zh"
/>
<label for="invalid-sms">仅短信</label>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="invalid-both"
name="notification-preference"
value="both"
class="input"
required
aria-describedby="notification-preference-error-zh"
/>
<label for="invalid-both">邮件和短信</label>
</div>
</div>
</fieldset>
<div class="flex flex-wrap gap-2">
<button id="notification-preference-submit-zh" type="submit" class="btn">
继续
</button>
<button
id="notification-preference-reset-zh"
type="reset"
class="btn"
data-variant="outline"
>
重置
</button>
</div>
</form>#RTL
在单选组或祖先元素上设置 dir="rtl"。原生键盘方向会跟随文档方向,选项的实际含义顺序仍由 DOM 决定。
تباعد قياسي لمعظم حالات الاستخدام.
مساحة أكبر بين العناصر.
تباعد أدنى للتخطيطات الكثيفة.
<div
dir="rtl"
role="radiogroup"
aria-labelledby="density-label-rtl"
class="radio-group w-fit"
>
<span id="density-label-rtl" class="label">
كثافة العرض
</span>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="r1-rtl"
name="radio-rtl"
value="default"
class="input"
/>
<section>
<label for="r1-rtl">افتراضي</label>
<p>تباعد قياسي لمعظم حالات الاستخدام.</p>
</section>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="r2-rtl"
name="radio-rtl"
value="comfortable"
class="input"
defaultChecked
/>
<section>
<label for="r2-rtl">مريح</label>
<p>مساحة أكبر بين العناصر.</p>
</section>
</div>
<div class="field" data-orientation="horizontal">
<input
type="radio"
id="r3-rtl"
name="radio-rtl"
value="compact"
class="input"
/>
<section>
<label for="r3-rtl">مضغوط</label>
<p>تباعد أدنى للتخطيطات الكثيفة.</p>
</section>
</div>
</div>#无障碍
- 为整组选择提供一个简洁的无障碍名称。表单内优先使用
fieldset与legend;其他场景使用role="radiogroup"搭配aria-labelledby或aria-label。 - 每个单选按钮都需要可见
label和稳定的提交value。说明文字通过aria-describedby关联,不要把长段说明塞进无障碍名称。 - 互斥选项必须使用同一个
name。不要通过彼此独立的按钮或只存在于应用状态中的逻辑模拟互斥。 - 保留原生方向键行为。Tab 进入当前选项,方向键移动并选择,Tab 离开整组;不要让每个选项都成为独立 Tab 停靠点。
- 不要给每个选项行添加
role="group"。radiogroup 或 fieldset 是唯一的分组边界。 - 只给不可用选项设置
disabled并保持原因可见。如果全部选项都不可用,应禁用所属表单操作并说明阻塞条件。 - 完成校验后再显示错误。使用
required、aria-invalid="true"与aria-describedby关联可恢复的错误说明,并在用户选择合法值或重置表单后清除过期的错误属性与文案。 - 在粗指针设备上让完整点击目标至少达到 44px,视觉圆点仍保持紧凑。
