提交值、校验字段,并用 Liquid Glass 控件组合可访问的 React 表单。
表单控件优先遵循原生 <form> 行为。Form 渲染原生 <form>,并在其上提供统一错误与表单级校验。Input 渲染原生 <input>;Checkbox、
CheckboxGroup、Switch、Select、Slider 与 RadioGroup 则渲染内部原生
输入用于提交。因此 FormData、Server Actions 与渐进增强模式都能继续使用,
不需要自定义状态容器。
Form 渲染原生 <form>,并使用组件库的纵向布局。通过 errors 把服务端错误发送给按名称匹配的 Field,通过 validationMode 设置内部字段的默认校验时机。Form 不会取代 FormData:原生 onSubmit 仍然接收浏览器提交的值,而 onFormSubmit 提供通过 Field 注册的控件对象。
<Form
action={savePreferences}
errors={serverErrors}
validationMode="onBlur"
>
<Field label="邮箱" name="email">
<Input type="email" required />
</Field>
<Button type="submit" label="保存" />
</Form>| 组件 | 提交形式 | 空状态 |
|---|---|---|
Input | 由 name 命名的原生输入 | 空字符串 |
Checkbox | 带 name 与 value 的隐藏 checkbox | 未选中时为 null |
CheckboxGroup | 每个选中值一个隐藏 checkbox,共享同一个 name | 未选中时没有条目 |
Switch | 带 name 与 value 的隐藏 checkbox | 关闭时为 null |
Select | 由 name 命名、携带选中值的隐藏原生输入 | 未选择时为 null |
Slider | 由 name 命名的隐藏 input[type="range"] | 当前值 |
RadioGroup | 由 name 命名、携带选中项 value 的隐藏 radio | 未选择时为 null |
RadioGroup 会提交选中条目的 value;必选时设置 required。Select 的行为
相同:name 启用提交,required 参与原生校验,form 可把隐藏输入关联到文档
其他位置的表单。受控 value 为 null 表示未选择任何值。
CheckboxGroup 会在同一个 name 下,为每个选中值提交一个条目,因此请用
FormData.getAll(name) 读取。复选框组没有原生 required,请在提交处理函数中
校验数组至少包含一个值。
Button 会把 type 转发给根 button 元素。需要提交表单时设置
type="submit"。
<form action={savePreferences}>
<Field label="邮箱">
<Input name="email" type="email" required />
</Field>
<Select
label="套餐"
name="plan"
required
items={[
{ value: "starter", label: "Starter" },
{ value: "team", label: "Team" },
]}
/>
<CheckboxGroup
label="通知渠道"
name="channels"
items={[
{ value: "comments", label: "评论" },
{ value: "releases", label: "版本发布" },
]}
/>
<Switch label="通知" name="notifications" value="enabled" />
<Button type="submit" label="保存" />
</form>在客户端处理提交时,从 FormData 读取值:
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = String(data.get("email") ?? "");
const channels = data.getAll("channels").map(String);
const notifications = data.get("notifications") !== null;
// 将值发送到 API,或更新本地状态。
}文本输入使用 Field,相关字段组使用 Fieldset,其他控件使用内置的标签属性:
Field 会为 Input 渲染真实的 <label>、说明与校验信息。Fieldset 使用共享的 legend 组织相关字段;每个 Field 保留自己的标签。Checkbox 与 Switch 使用可见标签作为可访问名称。CheckboxGroup 使用 label 为分组命名,每个选项使用自己的 label。Select 使用 label 为触发器命名;没有可见标签时,请使用 aria-label 或
aria-labelledby。RadioGroup 使用 label 属性为分组命名。Slider 不渲染可见标签,因此请用 aria-labelledby 指向外部标签。描述会通过 aria-describedby 自动关联。如果传入自己的
aria-describedby,它会与自动生成的值合并。
<div className="flex flex-col gap-2">
<span id="volume-label" className="text-sm text-ui-ink">
音量
</span>
<Slider aria-labelledby="volume-label" name="volume" defaultValue={40} />
</div>对于一组相关控件,使用 Fieldset 与 legend:
<Fieldset legend="通知">
<Switch label="邮件" name="email-notifications" value="on" />
<Switch label="@ 提及" name="mention-notifications" value="on" />
</Fieldset>每个有状态控件都支持两种模式:
// 非受控:组件自己持有状态。
<Field label="姓名">
<Input name="name" defaultValue="Ada" />
</Field>
<Switch label="公开资料" name="public" defaultChecked />
// 受控:由你持有状态。
const [email, setEmail] = useState("");
<Field label="邮箱">
<Input
name="email"
value={email}
onValueChange={setEmail}
/>
</Field>不要只传 value 而不提供变更处理函数。对于 Slider,onValueChange 会收到
当前数字或数组;只关心稳定值时使用 onValueCommitted。Select 使用 value 与
onValueChange;把 value 设为 null 可以清空选择。CheckboxGroup 使用 value
与 onValueChange,回调会收到下一次的选中值数组。
required、minLength、pattern、type 等原生约束会转发到输入框。自定义
校验时,把错误信息放在字段旁边,并用 aria-describedby 或
aria-errormessage 关联:
import { useState } from "react";
import { Field, Input } from "@xingxingmofashu/ui";
function EmailField() {
const [email, setEmail] = useState("");
const invalid = email.length > 0 && !email.includes("@");
return (
<Field
label="邮箱"
error={invalid ? "请输入有效的邮箱地址。" : undefined}
>
<Input
name="email"
type="email"
value={email}
onValueChange={setEmail}
/>
</Field>
);
}Slider 会把 aria-invalid 与 aria-errormessage 转发到内部的 range
输入,因此校验状态能到达可聚焦控件。Checkbox 与 Switch 会转发属性与
隐藏输入,同样适用。
disabled 阻止交互,并从表单提交中移除控件。readOnly 保留焦点与选择能力,但阻止修改。Button 的 focusableWhenDisabled 会让禁用操作保留在 Tab 顺序中,同时暴露
aria-disabled 并阻止激活。用户需要复制值时使用只读;当前状态下不相关的控件使用禁用。
由于控件通过原生输入提交,React 19 Server Action 可以直接接收普通
FormData:
"use server";
export async function savePreferences(formData: FormData) {
const email = String(formData.get("email") ?? "");
const notifications = formData.get("notifications") !== null;
// 持久化这些值。
}action 属性属于 Form 或外层原生 <form>;组件只负责提供字段。
value 而不传 onValueChange 会让受控字段冻结。FormData 会得到旧值或空值。Switch 提交 null,而不是 "false",请显式归一化。Select 不会提交值;在把 FormData 中的缺失条目当作字符串使用前,先按 null 处理。CheckboxGroup 会提交重复字段,请使用 getAll 而不是 get。CheckboxGroup 没有原生 required,请自行校验数组长度。Slider 需要可访问名称;标签渲染在组件外部时,aria-labelledby 通常最清晰。<label> 包裹控件。Input 使用 Field,相关字段组使用 Fieldset,其他控件使用内置的标签属性。