指南

表单

提交值、校验字段,并用 Liquid Glass 控件组合可访问的 React 表单。

表单控件优先遵循原生 <form> 行为。Form 渲染原生 <form>,并在其上提供统一错误与表单级校验。Input 渲染原生 <input>;Checkbox、 CheckboxGroup、Switch、Select、Slider 与 RadioGroup 则渲染内部原生 输入用于提交。因此 FormData、Server Actions 与渐进增强模式都能继续使用, 不需要自定义状态容器。

Used for account notices and receipts.

Preferences
Weekly digestProduct notes and release highlights, once a week.
ChannelsChoose where release notes are sent.
CommentsReleases
NotificationsSend a summary when a teammate publishes changes.
Volume

The slider submits its current value through a hidden range input.

Not submitted yet.

表单容器

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 并阻止激活。

用户需要复制值时使用只读;当前状态下不相关的控件使用禁用。

Server Actions

由于控件通过原生输入提交,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 会让受控字段冻结。
  • 在 submit 事件之前读取 FormData 会得到旧值或空值。
  • 关闭的 Switch 提交 null,而不是 "false",请显式归一化。
  • 未选择的 Select 不会提交值;在把 FormData 中的缺失条目当作字符串使用前,先按 null 处理。
  • CheckboxGroup 会提交重复字段,请使用 getAll 而不是 get。
  • CheckboxGroup 没有原生 required,请自行校验数组长度。
  • Slider 需要可访问名称;标签渲染在组件外部时,aria-labelledby 通常最清晰。
  • 不要自己用 <label> 包裹控件。Input 使用 Field,相关字段组使用 Fieldset,其他控件使用内置的标签属性。

相关组件