用途#
收集输入。
证据说明。 官网几乎没有表单控件(只有账号 SDK 的登录框,那是另一套样式,见 账号 SDK 弹窗,数值在云游戏页实测 11.6、11.7,measurements-cloud.md)。本页除了个别有实测依据的地方,其余都是按整体语言做的推断——可用、自洽,但不是官方界面的对应。实现时可以调整。账号 SDK 是鹰角各游戏共用的组件,它的形状一样都不取;下文只在几处记它做参照,标"实测(账号 SDK)"。
实现状态#
| 控件 | 写法 | 状态 |
|---|---|---|
| 字段 | <Field label help error required>;一组控件用 <Field group> |
已实现 |
| 输入框 | <Input variant size start end invalid> |
已实现 |
| 多行文本 | <Textarea variant showCount maxLength> |
已实现 |
| 验证码输入 | <OtpInput length value type groupSize mask name onComplete> |
已实现 |
| 复选框 | <Checkbox indeterminate> |
已实现(方案 A,可整体切到方案 B) |
| 单选 | <RadioGroup> + <Radio value> |
已实现(方案 A,可整体切到方案 B) |
| 开关 | <Switch>;两个值都要显示时加 offLabel onLabel |
已实现 |
| 筛选胶囊 | <FilterChip>,见 数据展示 |
已实现 |
| 下拉选择 | <Select items value placeholder variant size panelVariant invalid>;分组用 SelectGroup / SelectItem / SelectSeparator |
已实现 |
| 组合框 | <Combobox items value multiple loading filter>,属性比照下拉选择 |
已实现 |
| 标签输入 | <TagInput value name max separators validate> |
已实现 |
| 步进器 | <Stepper value min max step size> |
已实现 |
| 分段选择 | <SegmentedControl value name size variant> + <Segment value> |
已实现 |
| 滑块 | <Slider value min max step showValue marks>;value 传两个数是范围滑块 |
已实现 |
| 日期选择 | <DatePicker value min max format>;选一段用 <DateRangePicker>;只要月历用 <Calendar> |
已实现 |
| 文件上传 | <FileUpload value multiple accept maxSize maxFiles name>;单独的一行用 <FileItem> |
已实现 |
| 菱形符号(方案 B) | <html data-choice="diamond"> |
已实现 |
实现时对下文规格做的调整都写在各节里,并标了"实现"。
设计思路#
把表单控件当成"图纸上的填写栏":
- 凹陷的底 + 一条底边线,而不是四边描边的圆角框。
- 直角。
- 状态靠底边线的颜色变化:默认灰、聚焦墨、错误红。
- 选中的控件是墨底配黄色记号,和"墨底黄图标"的工具按钮同一个逻辑。
输入框#
| 项 | 值 |
|---|---|
| 高度 | 32 / 40 / 56px |
| 底 | surface-sunken |
| 底边线 | 2px line-strong |
| 圆角 | 0 |
| 水平内边距 | 12px |
| 文字 | text-base,ink |
| 占位文字 | ink-tertiary |
| 标签 | 在输入框上方,text-sm font-medium,间距 6px |
| 帮助 / 错误文字 | 在输入框下方,text-sm,间距 6px |
状态#
| 状态 | 底边线 | 其他 |
|---|---|---|
| 默认 | line-strong |
— |
| 悬停 | ink-secondary |
— |
| 聚焦 | ink,从左往右擦出 |
加焦点环;光标用 ink |
| 错误 | danger |
右侧菱形图标;下方红色说明文字 |
| 禁用 | line |
文字 ink-disabled;标签同步变浅 |
| 只读 | 无底边线 | 底变透明 |
聚焦的那条墨线不是把原来的底边线换个颜色,而是另画一条压在上面、从左往右伸开(--duration-base、ease-exit),失焦时缩回去(--duration-fast)——状态变了要看得出是"变过来的",和勾、圆点、括号这些状态的记号同一个道理。这些时候不画:错误态已经是红线,再盖一条墨线上去会把红盖掉;禁用和只读也不是"可以填"。打开的下拉面板(选择、日期、组合框)算作聚焦,线留着。(推断,见 动效)
为什么不用四边描边:line(#D9D9D9)在白底上只有 1.4:1,做输入框边界不够清楚;而加深到够清楚的描边又会让表单显得很重。凹陷底 + 加粗的底边线在两者之间取得平衡。
确实需要四边描边时(比如输入框放在 surface-sunken 的区域里),用 1px line-strong。
这一种写成 variant="outline"(默认是 sunken),输入框和多行文本都有:
| 项 | sunken |
outline |
|---|---|---|
| 边 | 只有底边,2px | 四边,1px |
| 底 | surface-sunken |
surface |
| 用在哪 | 页面与面板上,默认 | 凹陷底色的工具条、筛选栏里 |
两种变体的状态色完全相同(默认 line-strong → 悬停 ink-secondary → 错误 danger)。聚焦时的墨色在两种变体上都是那条另画的线:sunken 是一条压在底边线上的短线,outline 是整圈从左往右露出来(用裁切,不用缩放——缩放会把四条边压扁、角上变细)。outline 的底用 surface 而不是透明:放进凹陷的区域后,它要比周围亮一档才看得出是一个可以填写的格子。
实现:
- 字号跟随通用的三档:
sm用text-sm、md用text-base、lg用text-lg。 - 聚焦的墨线画在外框的
::before上,压在内容和底边线之上;外框因此带relative。分段选择的::after是选中的那块底,所以统一用::before。 - 焦点环画在外框上,不画在里面的
<input>上,前后缀因此也在环内。 - 悬停的底边线只在没有聚焦时生效,聚焦中的输入框不会因为鼠标经过而变浅。
- 只读时保留一条透明的底边线,和可编辑时一样高,切换时布局不跳。高对比模式下这条线写成
Canvas:系统会把透明的边线也换成字色显出来,而能填的输入框底色已经没了、同样只剩一条线,不写的话只读的和能填的就是一个样(outline是四边一圈)。只读又报错、只读又禁用时边线是错误 / 禁用的颜色,照旧留着。见 色彩。(推断) - 高对比模式下禁用的字色写在
<input>自己身上(GrayText):平时的ink-disabled是写在外框上、里面继承来的,系统换掉之后只有它自己认得的禁用控件才是灰字色,而没写type的<input>它不认——字和"已填写"的一个颜色。多行文本、组合框、标签输入里的那一个用的是同一个类;多行文本和标签输入右下的计数是<span>,同样写明。占位文字不用管,系统本来就把它画成灰字色。见 色彩。(推断)
前后缀#
图标、单位、清除按钮放在输入框内部的左右两端,用 ink-secondary。数字输入的单位("件""%")用 text-sm。
账号 SDK 的登录框是一个例子:输入框左端一个灰色图标,右端放"发送验证码"文字钮,前面隔一根短竖线,悬停时字变成高亮的 #E9BA15(实测(账号 SDK))。只作"行内动作放在后缀里"的参照,输入框的形状不取。
多行文本#
同样的底与底边线;最小高度 3 行;字数统计放在右下角,font-tech text-xs。
实现:字数统计放在框内、文字区域的下方,不压在文字上;有上限时写成 29 / 200。
验证码输入#
一格一位的短码:短信验证码、交接口令、六位的站点编号。(推断:就是把 输入框 切成一格一位——同样的凹陷底、同样的 2px 底边线、同样的状态色)
"一格一位、六格、当前格里一个闪烁的光标、当前格的线变深、出错变红、填了的格不另换样子"——这几样在账号 SDK 的设备验证层里都有:实测(账号 SDK,设备验证层,只读规则;这一层一打开就会发短信,没有打开它)。见 账号 SDK 弹窗、云游戏页实测 11.7(measurements-cloud.md)。它的格子是 38.4 × 48px、3.84px 圆角、0.8px #EEEEEE 的内描边(1440 宽),这个形状不取:本库见方、凹陷底、2px 底边线的格子仍是推断。
| 项 | 值 |
|---|---|
| 格子 | 见方,32 / 40 / 56px 三档;surface-sunken 底 + 2px 底边线,直角 |
| 字 | 居中,font-tech、等宽数字、font-medium;三档是 text-base / text-lg / text-2xl |
| 间距 | 格子之间 8px(sm 是 6px) |
| 分组 | 可选:每几格之间多一道 8px 宽、2px 高的短横(line-strong)。六位的码分成三位一组好念、好对。短横是底色画的,高对比模式下换成 CanvasText,不然只剩一段比别处宽的空(推断,见 色彩) |
| 状态 | 和输入框一样只看底边线:默认 line-strong → 悬停 ink-secondary → 错误 danger;聚焦时另画一条墨线,同样从左往右擦出。聚焦的那一格另有焦点环 |
| 禁用 / 只读 | 同输入框:禁用是 line 的底边线和 ink-disabled 的字;只读没有底边线、底变透明(高对比模式下那条占位的透明边线同样写成 Canvas) |
outline |
同输入框的 outline:四边 1px、surface 底,放在凹陷底色的区域里用 |
- 空格子就是空的,不放占位的圆点或横线:格子自己已经说明了"这里要填一位",再放一个记号会被看成已经填了。
- 填没填满不换颜色。填了的那一格里有字,这就够了。
- 填进去的那一位是淡入的(200ms):空格的字色是透明的,填上才是
ink,跟着格子本来就有的颜色过渡走。粘贴一整串时各格一起到,不错开。(推断,见 动效) - 容器窄了格子等比变窄(高度不变),不换行、不溢出:六格的
lg在 320px 宽的屏上也排得下。 - 只收数字,或者字母加数字。要收别的(带符号的密码)用输入框。
已实现为 <OtpInput length value defaultValue onValueChange onComplete type uppercase mask groupSize size variant invalid disabled readOnly required name autoSubmit slotLabel>:
<Field label="交接口令" help="六位数字,在交接单的右上角。">
<OtpInput length={6} groupSize={3} name="code" onComplete={verify} />
</Field>- 行为建立在 Base UI 的 OTP Field 上。每一格是一个真的输入框:打一位自动跳到下一格;
Backspace删掉这一位并退一格;←→在格子之间走,HomeEnd到两头;整组在Tab顺序里只停一次。 - 聚焦的墨线画在包着输入框的那层盒子上:
<input>是替换元素,自己画不了伪元素。见上面"输入框"的实现说明。 - 粘贴一整串会分到各格,里面的空白去掉;不合规的字符不收(
type="numeric"只收数字,是默认;"alphanumeric"收字母和数字)。uppercase把字母统一成大写——值也是大写,不只是看起来。 - 第一格带
autocomplete="one-time-code":手机收到短信验证码时,键盘上方那一条"来自信息"可以直接填进来。数字的那种会唤出数字键盘。 - 填满时调
onComplete(value);autoSubmit是填满就提交所在的表单。mask把字遮成圆点。 - 值是拼起来的一个字符串。带
name时有一个隐藏字段随表单提交。 - 读屏:第一格的名称是字段的标签(放进
Field自动关联,或者自己传aria-label);后面每格是"第 2 位,共 6 位"(slotLabel可换)。分组的短横是装饰,读屏不读。 className给外面那一排,ref也是。
没有做的:
- 重发倒计时。那是页面的事:旁边放一个按钮,上面用现成的
Countdown。账号 SDK 的设备验证层也是这样:格子之外另有一行"获取验证码",倒计时中写成"N 秒后可重新获取验证码",数字等宽,60 秒(实测(账号 SDK),只读规则)。 - 每一格各自的错误态:对不对是整串码的事,
invalid一亮六格一起亮。
下拉选择#
触发器:外观同输入框,右侧一个实心倒三角。
面板:
| 项 | 值 | 来源 |
|---|---|---|
| 底 | surface-raised + 1px line;或深色(surface-inverse) |
推断 / 实测 |
| 顶部强调条 | 一条半宽的 action 色细条,高 3 – 4px |
实测(官网语言菜单:顶部 0.375rem 的黄条,宽 50%) |
| 条上的三角 | 条的下沿挂一个朝下的小三角,露出 8 × 4px,中心距条的左端 32px | 同 下拉菜单:取自实机风格弹窗的分隔条,按条高减半 |
| 选项高 | 32 – 36px | 推断 |
| 悬停 | 提亮一档(ink/5) |
推断 |
| 当前项 | surface-muted + 左缘墨色条 + 加粗;深色面板里是整行黄底墨字 |
推断 / 实测 |
| 圆角、分隔线 | 都没有 | 没有分隔线:实测;直角:推断 |
| 阴影 | shadow-sm |
推断 |
官网的语言下拉是深色面板 + 黄色当前项(实测,黄色取值是偏金的 #FFCC1A)。它的面板带 1rem 的圆角、选项是胶囊,这两样没有照搬:面板和选项取直角,和别的浮层一致。顶上那条黄条在官网是居中的、贴在面板上沿的外面,这里靠左、压在上边线上。本规范提供浅、深两种面板,默认跟随主题。
实现:
- 触发器直接用输入框的外框(
control-box.ts):两种变体、三档尺寸、各状态的边线颜色都和输入框一样,并排放不会走样。焦点环同样画在外框上。面板开着的时候边线保持聚焦的墨色。 - 右侧的实心三角闭合时朝下,展开时翻成朝上。
- 面板落在触发器下方,和它等宽、左对齐,离它 4px;下面放不下时翻到上面。没有用"面板盖住触发器、当前项对齐到原位"的那种做法——顶部的强调条需要一个固定的位置。
- 面板最高 320px,选项多了在面板里滚动。
- 面板的画法和 下拉菜单 共用,进出也一样(淡入,从触发器那一侧挪过来 4px;退场只淡出):
panelVariant="plain"(默认,跟随主题)或"strong"(固定深色,当前项整行黄底墨字)。悬停底从推断的surface-sunken改成了全库统一的ink/5:前者在暗色主题下是纯黑,悬停会变暗而当前项变亮,方向相反。 - 放进
Field后标签、帮助文字、错误说明自动关联。点标签会打开面板——触发器是一个按钮,点它的标签等于点它。 - 带
name时选中的值随表单提交。值的类型是字符串。 - 键盘:回车、空格、上下方向键打开;方向键移动;回车选中;按字母跳到以它开头的项;
Esc关闭。选中或关闭后焦点回到触发器。 - 打开时焦点落在当前项上。深色面板里它是一整行黄底,焦点环因此用
on-action、离边 2px,见 下拉菜单 的实现。(推断) - 选项只有两三个、又都该一眼看到时,用单选或页签,不用下拉。
- 高对比模式下当前项的底色没了:两种面板的当前项都是左缘一条 3px 的
Highlight竖条(深色面板平时没有这一条,只在这时候画)。键盘走到的那一项照旧有焦点环。见 色彩。(推断)
多选#
加 multiple,值变成字符串数组。
- 选项行首是一个 16px 的小方格(和下拉菜单的复选项是同一个),勾上的项文字加重。不再用"当前项"的整行底色:单选时只有一行是当前项,多选时会有好几行,整片的底色会把面板涂花,也分不清哪些是悬停。
- 选了不关面板,可以连着选;点外面或按
Esc收起。 - 触发器里把已选项用顿号连起来,放不下时截断成省略号;选了两项以上时,后面跟一个不会被截掉的计数("3 项")。要换一种写法用
renderValue。 - 带
name时每个选中的值各提交一份,同一个字段名。 - 读屏:面板是一个可多选的列表框,每一项报出选没选。
- 选项很多、需要搜索时它不合适——那是 组合框 的事。
组合框#
一个能打字的下拉:选项多到要找的时候用它(推断——官网和游戏界面里没有这个控件,画法全部沿用输入框与下拉选择)。
| 下拉选择 | 组合框 | |
|---|---|---|
| 选项数量 | 十来个以内,扫一眼就能找到 | 几十个以上,或者使用者记得名字 |
| 触发器 | 一个按钮 | 一个输入框 |
| 找选项 | 方向键,或者按首字母跳 | 打字筛选 |
值只能是选项里有的。它不是"带建议的输入框":打了一半没选就离开,输入框会回到原来的值。
已实现为 <Combobox items value onValueChange>,属性比照下拉选择:
- 外框就是输入框的外框:两种变体、三档尺寸、各状态的边线颜色都一样。里面从左到右是输入框、错误图标、清除钮(有值时才出现)、和下拉选择一样的实心三角。
- 面板、选项、分组小标题和下拉选择是同一份画法,
panelVariant同样有plain和strong。一个都没匹配上时,面板里是一行次要色的说明(emptyText,默认"没有匹配的选项"),面板不会空着或者消失。 items是{ value, label, keywords?, disabled? };要分组就传{ label, items }的数组。label必须是字符串——要拿它检索。- 检索匹配
label和keywords,不分大小写。keywords放别名:编号、缩写、旧称,它们不显示。拼音检索没有做;需要的话把拼音放进keywords。 - "移到了哪一项"只靠底色表示。 焦点一直留在输入框里(读屏靠
aria-activedescendant知道移到了哪),选项上没有焦点环可画。所以当前项被移到时底色再深一档(line;深色面板里是action-pressed),否则它原有的底色会把"移到"盖住。 - 高对比模式下底色都被系统换掉,"移到了哪一项"就看不见了:给被移到的那一项画一圈 2px 的
Highlight环,画在里面——和下拉选择、菜单里键盘走到的那一项同一个样子(那边是真的焦点环,这边跟着"移到"画)。见 色彩。(推断) - 键盘:打字即筛选并打开面板;
↓↑移动,回车选中;Esc先关面板,再按一次清空。 - 放进
Field后标签、帮助文字、错误说明自动关联。带name时选中的值随表单提交。 - 选项要从服务器取时见下面的"远程检索"。
远程检索#
选项多到不能一次给全时,由服务器按输入的文字返回:
<Combobox
items={results}
filter={false}
loading={pending}
onInputValueChange={search}
/>onInputValueChange拿到输入的文字,自己去取,取回来换items。filter={false}:不在本地再筛一遍,items给什么就显示什么。服务器按拼音、按编号匹配出来的结果,本地按文字再筛会被筛掉。loading:面板里出一行"正在查找…"(一个行内的加载方块 + 文字,loadingText可换),这时不显示"没有匹配的选项"。上一批选项照常留着,新的回来再换——面板不会先空一下。- 这一行和"没有匹配"那一行都是读屏的状态区,换内容时会播报。
- 选中的那一项不在后来的这批选项里也没关系:组合框记得见过的选项,输入框里的文字、提交的值都还在。只有一种情况找不到——值是从外面给的,而对应的选项从来没有出现在
items里;这时把那一项也放进items。 - 请求的防抖、取消过期的请求由使用方做。
多选#
加 multiple,值变成字符串数组。
- 已选项在框里排成一个个直角的小块:
surface-muted底,右端一个删除叉。用直角不用胶囊——它是已经定下来的值,不是会自己变的状态(见 数据展示 的"形状就是语义")。 - 小块放不下就换行,外框跟着长高:三档尺寸在多选时是最小高度。小块的高度是 20 / 24 / 32px,和它排在一起的输入框同高。
- 高对比模式下小块的底色被系统换掉,几个值连成了一串字,分不出哪儿到哪儿是一个:每个小块补一圈 1px 的
ButtonText线。禁用时这一圈线、字和叉都是GrayText——小块不是真的控件,系统不会自己换。标签输入里的小块是同一份画法。见 色彩。(推断) - 选项行首是和下拉多选一样的小方格。
- 选了之后面板关不关,看有没有打字:没打字直接从列表里选,面板不关,可以连着选;打了字搜出来再选,这一次搜索就结束了——文字清空、面板关上,焦点留在输入框里,接着打字会再打开。(这是基元的行为:搜索结果在选中后会整个换掉,留着面板反而跳。)
- 输入框空着时按
Backspace删掉最后一个;←能走到小块上,在小块上按Backspace或Delete删掉它。 Esc关面板的同时把没选完的字清掉(单选时要按第二次才清)。- 清除钮一次清空全部。
标签输入#
自己打出来的一串短词:关键词、标签、收件人。(推断:外框是输入框的,小块是上面组合框多选里的那一种)
| 项 | 值 |
|---|---|
| 外框 | 输入框的外框:两种变体、各状态的边线颜色都一样。三档尺寸是最小高度(32 / 40 / 56px),小块多了换行、外框长高 |
| 小块 | 直角,surface-muted 底、ink 字,高 20 / 24 / 32px;右端一个删除叉(ink-secondary,悬停 ink + line 底)。和组合框多选是同一份画法,高对比模式下那一圈线也是 |
| 计数 | 传了 max 才有:右端一个 3 / 8,font-tech、等宽、text-xs、ink-secondary;满了变成 ink。小块换了行它贴着最后一行(和多行文本的计数同一个位置) |
| 重复时 | 已有的那个小块闪一下:填充反转(surface-inverse 底、ink-inverse 字)400ms。不用黄色——这不是"选中"。高对比模式下底色被系统换掉、反转闪不出来,这 400ms 改成在小块外面套一圈 2px 的 Highlight 环(推断,见 色彩) |
已实现为 <TagInput value onValueChange placeholder name max maxLength validate separators commitOnBlur onReject variant size invalid disabled readOnly>:
- 怎么加:回车;或者打出分隔符(默认是半角和全角的逗号,
separators可换);粘贴一串带分隔符或换行的文字会拆成几个。离开输入框时,没提交的字也加进去(commitOnBlur,不要就关掉)。前后的空白去掉,空的不加。- 分隔符是在"框里的字变了"的时候处理的,不看按的是哪个键:中文输入法打出来的","、粘贴进来的,走的都是这一条。输入法正在组字时按的回车不算提交——那是在选字。
- 三种不加的情况都不是静悄悄的。每一种都会调
onReject(文字, 原因),读屏也会听到一句:"duplicate"已经有了:那一个小块闪一下,框里的字留着,方便改;"max"到了上限:计数已经是满的;"invalid"没过validate:validate返回一句话就是不让加,这句话交给onReject,由使用方放进Field的error(控件自己不画错误文字,那是字段的事)。
- 怎么删,键盘和组合框多选一致:框里空着时
Backspace删最后一个;←走到小块上,在小块上Backspace/Delete删掉它,焦点落到旁边那个,一个都不剩就回到框里;→走回框里。小块上的叉是一个按钮("移除某某"),鼠标和读屏的浏览光标都点得到,但Tab不停在上面——整个控件只占一个停靠点。 - 加了、删了、没加成,都有一句读屏听得到的播报("已添加北岭""已移除北岭""已经有北岭了")。
- 新加的小块淡入(
--duration-fast):一开始就有的那几个是静止的;粘贴一串拆出来的几个一起到,不错开。(推断,见 动效) - 删掉的小块原地淡出,同时后面的小块和输入框滑过来补位(都是
--duration-fast):走的那一块脱离排版、留在原处淡掉,别的从它上面滑过去。焦点不等它——删的那一刻就落到旁边那个上(规则见上面"怎么删"),正在淡出的那一块点不到、读屏读不到。连着删的时候各走各的,补位从眼前的位置接着走。少了一行时外框的高度跟着收(同样--duration-fast):还在滑的小块不会露到框外面,后面的内容是被带着上来的。小块到了 50 个就不做,照旧直接消失。(推断,见 动效) - 带
name时每个标签一个同名的隐藏字段,随表单提交。放进Field后标签、帮助文字、错误说明自动关联。 readOnly:只列出来,不能加也不能删,小块上没有叉。
没有做的:
- 候选列表(一边打一边提示已有的标签)。只能选现成的用组合框的多选;"能选也能新建"两者都不是,没有做。
- 拖动排序;标签带颜色或图标。
复选与单选#
两套符号方案。全库选一套,不要混用。
方案 A:方形与圆形(默认)#
| 控件 | 未选 | 已选 |
|---|---|---|
| 复选框 | 20px 方形,surface-sunken 底,2px line-strong 描边 |
墨底(surface-inverse)+ 黄色对勾 |
| 半选 | — | 墨底 + 黄色短横 |
| 单选框 | 20px 圆形,同上 | 墨底 + 黄色圆点 |
用户最熟悉,适合通用产品。默认方案。
账号 SDK 的协议勾选是一个 16px 的圆,勾上是 #FFD800 黄底配深色勾(实测(账号 SDK),1440 宽;深色的勾只读了规则)。不取:这里圆是单选的形状,复选框画成圆就和单选分不开了;整块黄底也是下面"忌"里的那一条。
方案 B:菱形#
| 控件 | 未选 | 已选 |
|---|---|---|
| 复选 / 单选 | ◇ 空心菱形 | ◆ 实心菱形 |
出自社区组件库 ReEnd,风格更强。问题是复选和单选长得一样,用户分不出"能选几个"——用它时必须靠分组标题或说明文字讲清楚。适合游戏风格的界面。
实现:它不是组件上的属性,而是和主题一样的全局开关——"全库选一套"由写法来保证:
<html data-theme="dark" data-choice="diamond">也可以写在局部容器上,容器里的复选与单选一起换。组件的用法、语义、键盘都不变。
| 状态 | 外观 |
|---|---|
| 未选 | 空心菱形:surface-sunken 底、2px line-strong 描边 |
| 已选 | 实心菱形,surface-inverse:亮色下是墨色,暗色下是近白。不再画对勾和圆点 |
| 半选(仅复选) | 实心菱形上一条 accent-ink-inverse 的短横 |
| 禁用 | 描边降到 ink-disabled;已选的保持实心(ink-disabled),否则和未选分不出来 |
| 错误 | 描边换成 danger |
- 菱形是把 14px 的方格转 45°,对角线正好还是 20px,行高与对齐都和方案 A 一样。
- 焦点环跟着菱形走,也是菱形的。
- 开关不受影响:它本来就不是方或圆的"选中符号"。
共同规则#
- 文字标签在控件右侧,间距 8px,点击标签等同点击控件。
- 点击区域不小于 40px 高。
- 禁用:描边与记号都降到
ink-disabled。 - 暗色主题下,已选态反过来:浅底(
surface-inverse此时是近白)+ 墨色记号,或直接用黄底墨色记号。
实现:
- 已选的底用
surface-inverse,记号用accent-ink-inverse——和面板标题带的竖条是同一条约定。亮色下是墨底黄记号;暗色下surface-inverse是近白,黄色压上去只有 1.1:1,记号自动换成强调色的深档(6.3:1),也就是上面说的"浅底 + 墨色记号"。 - 悬停时未选的描边加深到
ink-secondary;鼠标在文字标签上同样触发。 - 记号(勾、半选的短横、圆点)出现和消失是 200ms 的淡入淡出,和方格换底色的那一下同时走;半选和全选互换时两个记号交叉淡化。菱形方案没有记号,"已选"就是方块自己换底色。(推断,见 动效)
- 标签换行时,控件对齐第一行,不居中。
- 错误态描边换成
danger,同时必须有文字说明。 - 一组单选都没选时,浏览器会让它们全部命中
:indeterminate,所以半选的样式只写在复选框上。 - 高对比模式下反转块没了,单选的圆点、菱形方案的"已选"跟着看不见:已选(和半选)的方格 / 圆改用
Highlight,上面的记号用HighlightText;禁用的描边和记号用GrayText,菱形方案里禁用的"已选"仍然是实心的。菜单复选项、多选选项行首的小方格同理。见 色彩。(推断) - 高对比模式下禁用项的文字标签也要写明
GrayText:字色是写在<label>上的,系统不知道这一行禁用了,会把它当普通字换成页面的字色——方格是灰的,旁边的字却和没禁用的一样。开关的文字标签同理。(推断)
分段选择#
两到五个值里选一个,选项并排摆在一条轨道里,一眼看得全。(推断:官网和游戏界面里没有这个控件。轨道是输入框的"凹陷的底 + 一条底边线",选中的那一段是这套语言里的"填充反转"——和选中的胶囊、月历的选中格是同一个做法。)
| 项 | 值 |
|---|---|
| 轨道 | surface-sunken 底 + 2px 的底边线,直角,四周 2px 内边距 |
| 高度 | 32 / 40 / 56px,和输入框同三档:并排时对得齐 |
| 段 | 等宽,按最宽的那一段定;水平内边距 12 / 16 / 24px |
| 未选 | ink-secondary;悬停 ink + 墨色 5% 的底 |
| 选中 | surface-inverse 底、ink-inverse 字。底是轨道上的一块,换一段时滑过去 |
| 禁用 | ink-disabled;选中又禁用的那一段是 disabled 底、on-disabled 字 |
| 底边线 | 默认 line-strong,里面有焦点时 ink,错误 danger——和输入框一样靠这条线说状态 |
variant="outline"把轨道换成四边 1px 的描边加页面底色,放在凹陷底色的工具条里用,和输入框的两种变体一一对应。- 选中不加粗。 加粗会让那一段变宽,整条跟着跳。选中靠的是填充反转,不只是换个字色。
- 各段等宽,选中的块是同一块。 换一段时墨块沿轨道滑到新的一段(
--duration-fast),大小不变,字色同一拍换过来;值在选中的那一刻就变了,不等它滑到。连着换时从半路改道。第一次出现、轨道变了宽度都直接到位;从一个都没选到选了一个,是原地淡入。(推断,见 动效) - 默认按内容定宽;要撑满所在的一栏加
className="w-full"。放不下时每一段的文字截断成省略号,不换行、不撑破——所以每段两到四个字为好。 - 高对比模式下底色都被系统换掉,填充反转就看不出来了:墨块改用系统的"选中"色(
Highlight底、HighlightText字),选中又禁用的是GrayText底、Canvas字;没选中的禁用段字色写明GrayText——每一段是一个<label>,系统不会自己把它的字换成灰字色。见 色彩。(推断)
用哪个:
| 情况 | 用 |
|---|---|
| 两到五个短选项,要一眼看全 | 分段选择 |
| 选项要带说明、要竖排、或者多于五个 | 单选 或 下拉选择 |
| 切换的是下面显示哪一块内容 | 页签:它切的是内容,不是一个表单值 |
| 可以同时选几个 | 筛选胶囊 |
| 只有开和关 | 开关;两个值都要写出来时用双标签开关 |
已实现为 <SegmentedControl value defaultValue onValueChange name size variant disabled required> + <Segment value icon disabled>:
- 语义是单选组:每一段里是一个真的单选按钮。方向键换值(到头绕回去)、
Tab只停一次、带name时值随表单提交,都是浏览器原生的行为。 - 焦点环画在有焦点的那一段上,并提到相邻段之上。
- 选中的底画在轨道的
::after上,位置是量出来的,轨道里不多一个元素。量不到的时候(服务端渲染、还没排版、藏着)各段照旧自己画选中的底,不会有哪一帧没有选中态。 - 放进
Field后标签、帮助文字、错误说明、禁用、必填自动关联;不在Field里时自己传aria-label。 - 只有图标的段要给
aria-label。 - 只做了单选。能同时按下几个的那种(加粗、斜体)是工具栏里的开关按钮,不是表单值,这里没有做。
开关#
胶囊轨道 + 圆形旋钮。这是全库少数的圆形控件之一——"胶囊 = 短暂可变的状态"。
| 项 | 值 |
|---|---|
| 轨道 | 52 × 28px,全圆角 |
| 旋钮 | 20px 圆 |
| 关 | 轨道 neutral-400,旋钮 surface-raised |
| 开 | 轨道 surface-inverse,旋钮 action |
| 过渡 | 旋钮平移,--duration-fast |
"开"的状态是墨色轨道 + 黄色旋钮,而不是整条黄色轨道:黄色只点在旋钮上。
实现:
| 状态 | 轨道 | 旋钮 | 说明 |
|---|---|---|---|
| 关 | line-strong |
surface-raised |
亮色取值就是上表的 neutral-400;暗色是 #626262,不会比"开"更抢眼 |
| 关 · 悬停 | ink-tertiary |
同上 | — |
| 开 | surface-inverse |
accent-ink-inverse |
亮色是墨轨黄钮;暗色是近白轨道 + 深色旋钮,理由同复选框 |
| 禁用 | surface-muted |
ink-disabled |
开与关只靠旋钮位置区分 |
语义上是 <input type="checkbox" role="switch">,读屏读作"开关"。立即生效的设置用开关;要点"保存"才生效的用复选框。
高对比模式下轨道和旋钮都是底色画的,整个开关会不见(推断,见 色彩):
| 状态 | 轨道 | 旋钮 |
|---|---|---|
| 关 | 页面色,一圈 2px 的 ButtonText 边线 |
ButtonText |
| 开 | Highlight,实心 |
HighlightText |
| 禁用 | 关:一圈 GrayText 边线;开:GrayText 实心 |
关:GrayText;开:Canvas |
双标签开关#
两个值都要显示时(如 2D / 3D),用官网的切换器样式(实测):
- 半透明深色胶囊轨道(
rgba(0,0,0,.5)–.7); - 浅色圆形旋钮(
#E6E6E6),旋钮上写着当前值; - 另一个值写在轨道的空位上,低对比;
- 文字用
font-tech。
云游戏页"功能设置"里的开关也是这个样子:134 × 30 的胶囊,白 20% 的轨道上一块盖住半条的胶囊滑块,滑块上写当前值("开"或"关"),另一个字压在轨道上、颜色更淡;开时滑块 #EEEEEE,关时 #BFBFBF,滑块走 0.3s(实测(云游戏页),关的样子只读了规则)。这里的取值不变,见 云游戏页。
实现:给 Switch 同时传 offLabel 与 onLabel。
| 部位 | 取值 | 说明 |
|---|---|---|
| 轨道 | surface-inverse 的 80% |
两个值都是有效值,轨道不随开关变色。亮色下是上面的半透明深色;实测的 rgba(0,0,0,.5) 放到暗色页面上会和页面融在一起,所以跟着主题反转成浅色轨道 |
| 旋钮 | surface-muted |
亮色下正好是实测的 #E6E6E6 |
| 旋钮上的值 | ink |
— |
| 轨道上的另一个值 | ink-inverse 的 70% |
"低对比"但仍然读得清:两个主题下都在 4.5:1 以上 |
| 禁用 | 轨道 surface-muted,旋钮 surface-raised,字 ink-disabled |
— |
- 两个值各占一格、等宽,按较宽的那个定宽;旋钮是盖住半条轨道的胶囊,所以两个汉字("列表" / "网格")也放得下。
- 语义仍然是开关:读屏读到的是"开关,开 / 关",轨道上的字对它隐藏。名称要写成"开 = 第二个值"的说法,比如
aria-label="三维视图",而不是"视图"。 - 高对比模式下轨道只剩一圈
ButtonText的边线,旋钮是Highlight;旋钮上的值用HighlightText,轨道上的另一个值用CanvasText。禁用时旋钮是GrayText、上面的值是Canvas。见 色彩。(推断)
步进器#
− 按钮 + 数字 + + 按钮,三段紧贴。
- 两端是
control色的方形按钮; - 中间是
surface-sunken底,数字font-tech、等宽、加粗、居中; - 到达上下限时对应按钮禁用。
已实现为 <Stepper value min max step size>,高度是通用的三档(32 / 40 / 56px),两端按钮是等高的正方形:
- 中间可以直接输入。 输入到一半不打断(敲
5不会因为下限是 10 而被立刻改掉),失焦或回车时才落定,并夹到范围内;清空后失焦还原成原来的值;字母进不了输入框。 - 键盘:
↑↓加减一步,PageUpPageDown十步,HomeEnd到上下限(没设上下限时不响应)。语义是role="spinbutton",带aria-valuenow/aria-valuemin/aria-valuemax。 - 两端的按钮不占 Tab 顺序:键盘用户在输入框里按方向键就够了,免得一个数字要 Tab 三次。按钮仍然有名称("减少""增加"),读屏和触屏照常能用。
- 按住连续加减:按下先走一步,400ms 后每 80ms 一步,松开、移出或到头时停。
- 小数步长(
step={0.1})按步长的小数位数取整,不会出现0.30000000000000004。 - 颜色:两端
control/on-control(两个主题下相同),禁用时disabled/on-disabled;错误态在数字下方出现一条 2px 的danger线,同时必须有文字说明。 - 放进
Field后标签、帮助文字、错误说明自动关联;焦点环画在整个步进器的外框上。 - 高对比模式下两端按钮和中间的底色都被系统换掉,只剩
−、数字、+三样浮着,看不出按钮到哪儿为止:两端的按钮各补一圈 1px 的ButtonText线,中间上下各一条,连成三格。到头而禁用的那个按钮,记号系统自己会换成灰字色,那一圈线要写明GrayText。见 色彩。(推断)
滑块#
在一个范围里拖着选一个数:音量、缩放、透明度。(推断。)
| 步进器 | 滑块 | |
|---|---|---|
| 要的是 | 一个确切的数 | 一个大概的位置 |
| 改的时候 | 一步一步地加减,或者直接输入 | 边拖边看效果 |
| 范围 | 可以没有上下限 | 必须有上下限 |
| 项 | 值 |
|---|---|
| 轨道 | 高 4px,line,直角 |
| 走过的一段 | ink。不用进度条的黄色:滑块的位置要看得清,黄色在浅灰轨道上看不清 |
| 滑块 | 12 × 20px 的直角墨块(surface-inverse),和列表项、菜单当前项左缘的强调竖条是一家;四周一圈 2px 的 surface 色,把它和轨道分开 |
| 按住 | 墨块正中亮一条 2px 的强调色细线(accent-ink-inverse),200ms 淡入淡出 |
| 聚焦 | 滑块外 2px 的 focus 色环 |
| 数值 | 可选,在轨道右侧,font-tech、等宽、加粗 |
| 刻度 | 可选,轨道下方:1px × 6px 的短线(line-strong)+ text-xs 的标注 |
| 高度 | 整个控件占 32 / 40px(sm / md),和同一行的其他控件对齐;轨道居中 |
| 禁用 | 走过的一段和滑块变 ink-disabled |
- 切到菱形方案(
data-choice="diamond")时,滑块换成一个 14px 的菱形。 - 触屏上滑块的点击区补到 40 × 40px。
- 点轨道的任意位置,滑块跳过去并且可以接着拖。
- 按键和外部改值时滑过去,拖动时不过渡。 方向键、
PageUp/PageDown、Home/End,或者外面把值改了,滑块和走过的一段在 200ms 里滑到新位置。拖动中位置贴着指针,没有过渡——加了的话滑块会追着手指跑。点轨道也没有:按下的那一刻就已经算开始拖了。(推断,见 动效) - 高对比模式下轨道、走过的一段、滑块都是底色画的,整个控件会不见,只剩数值:轨道剩一圈 1px 的
ButtonText线(空心),走过的一段是Highlight的实心条,滑块是ButtonText的实心块,刻度的短线也是ButtonText。走过和没走过的是实心对空心,不靠两种系统色分。禁用时这几样连同数值都是GrayText。见 色彩。(推断)
实现#
<Slider value defaultValue onValueChange onValueCommitted min max step largeStep size showValue format marks name disabled>。默认范围 0 – 100、步长 1。value传两个数就是范围滑块:两个滑块,走过的一段在它们之间;minStepsBetweenValues规定两者至少隔几步,两个滑块互相推不过去。onValueChange在拖动的每一步都触发;onValueCommitted只在松手(或一次按键)之后触发——要发请求的事放在后者里。showValue在右侧显示当前值;format是Intl.NumberFormat的选项(百分比、单位、小数位)。范围滑块显示成20 – 60。数值区按最宽的那个值留宽,拖动时轨道不会跟着伸缩。marks是{ value, label? }的数组。刻度只是标注,不改变步长;要"只能停在刻度上",把step设成刻度的间距。- 键盘:
←↓减一步,→↑加一步;PageUpPageDown(或Shift+ 方向键)走largeStep(默认 10);HomeEnd到两端。 - 语义是原生的范围输入:每个滑块里有一个真的
<input type="range">,读屏读到的是"滑块,40,最小 0,最大 100"。带name时值随表单提交(范围滑块提交两个同名的值)。 - 放进
Field后标签自动关联。范围滑块是一个以标签命名的分组,两个滑块各叫"最小值""最大值"(thumbLabels可换)。不在Field里时自己传aria-label。 - 一处和别的字段不一样:点标签不会把焦点移到滑块上。滑块里那个原生输入的
id是无障碍基元自己生成的,标签的for指不到它;名称是用aria-labelledby关联的,读屏不受影响。 - 轨道两端各缩进半个滑块的宽度:滑块到头时它的外缘和别的字段对齐,不伸到外面去。只做了横向的。
日期选择#
选一天,或者选一段。(推断:官网和游戏界面里没有日期控件;月历的画法取自这套语言里"格子"的做法——直角、等宽数字、选中是填充反转。)
值是 YYYY-MM-DD 的字符串,和原生的 <input type="date"> 一样:没有时间,也没有时区,存取和比较都不会差出一天。
月历#
| 项 | 值 |
|---|---|
| 结构 | 一行月份标题和一对翻月的钮;一行星期;六行日期 |
| 月份标题 | 2026年10月,text-sm、加粗;居中 |
| 翻月钮 | 32px 的方形 图标按钮,在标题两侧 |
| 星期行 | text-xs,ink-tertiary,一个字(一、二、三…) |
| 日期格 | 40px 见方,直角,没有间距;数字 font-tech、等宽、text-sm |
| 悬停 | ink 5% |
| 选中 | 填充反转:surface-inverse 底、ink-inverse 字,加粗 |
| 今天 | 数字下面一条 12 × 2px 的强调色短线(accent-ink;压在选中格上时是 accent-ink-inverse) |
| 键盘聚焦 | 2px 的焦点环,画在格子里面;压在选中格上时是 accent-ink-inverse |
| 不在本月 | ink-tertiary;仍然可以点,点了翻到那个月 |
| 不可选 | ink-disabled 加删除线 |
| 行数 | 永远六行:翻月时高度不跳 |
- "今天"不只靠颜色:它是一条线,不是换个字色。选中和今天可以是同一天,两个记号各管各的。
- 选中格的焦点环换了颜色:日期格之间没有间距,环只能画在格子里面,也就压在了选中的实心块上。普通的焦点色在亮色主题下就是那块墨的颜色,环完全看不见;暗色下是黄压在近白上,也只差一点。所以选中格的环用
accent-ink-inverse,和压在它上面的"今天"短线同一个颜色——亮色是墨块上的黄,暗色是近白块上的深档。选一段时的两端也是。(推断:同 全屏菜单 当前项的做法) - 一周从星期一开始(
weekStartsOn={0}改成星期日)。 - 月份标题、星期、读屏听到的日期名称按
locale走(默认zh-CN),用的是浏览器的Intl。 - 高对比模式下选中格改用系统的"选中"色(
Highlight底、HighlightText字);"今天"的短线也是底色画的,换成CanvasText,压在选中格上时是HighlightText。画在选中格里面的焦点环用HighlightText,再往里收 2px。见 色彩。(推断)
已实现为 <Calendar value onValueChange month onMonthChange min max isDateDisabled weekStartsOn locale today>:
- 键盘(ARIA 实践指南的日期网格):方向键走一天 / 一周;
HomeEnd到这一周的头尾;PageUpPageDown换月,加Shift换年;回车或空格选中。走出当前这个月时月历跟着翻。 - 整个网格只占一个
Tab停靠点:停在选中的那一天,没选时是今天。 minmax之外的日子、isDateDisabled返回真的日子不可选。键盘走到范围外会被夹回边界;不可选的日子仍然走得到(读屏要能读到它为什么不能选),只是选不了。整个月都在范围外时,对应的翻月钮禁用。- 语义是一张
role="grid"的表,以月份标题命名;每个日子是一个按钮,名称是完整的日期("2026年10月9日星期五"),选中的那一格带aria-selected,今天带aria-current="date"。翻月时标题会播报。 today默认取本地时间的今天;要在服务端渲染、或者要一个固定的"今天"时自己传。- 翻月时新的一个月从翻的那一侧来:往后翻从右、往前翻从左,位移 8px 并淡入(
--duration-base);旧的直接换掉,月份标题和星期那一行不动。翻月钮、键盘走出这个月、PageUpPageDown、点不在本月的日子、从外面换了月份,都是这样;载入时就显示着的那个月是静止的。连着翻不播——上一次的还没播完又翻了(按住PageDown、连点翻月钮),这一次直接换:每次都从透明起的话,格子会一直闪。焦点不受影响,格子没有重挂;翻月和移焦点都是当场生效的,不等动效。(推断,见 动效)
日期选择#
一个字段:点开是一块面板,里面是月历。
- 外框就是输入框的外框(两种变体、三档尺寸、各状态的边线),里面是日期和一个日历图标。日期写成
2026.10.09——和日期块、微标行是同一种写法;format可换。 - 高对比模式下日历图标的颜色写明系统色:
ButtonText,禁用时GrayText。它的灰是写在<svg>自己身上的,系统不换——暗的那套配色下是深灰压黑底,几乎看不见;禁用时旁边的日期是灰字色,它却比没禁用的还亮。日期范围用的是同一个外框。见 色彩。(推断) - 触发器是一个按钮,不能打字。 生日这类离今天很远、使用者又记得住的日期,直接打字比翻月历快——那种字段用原生的
<Input type="date">。 - 面板是 气泡卡片 的那一块:
surface-raised+ 1pxline+ 顶部半宽的强调条,在外框下方、左对齐。底下一行两个文字钮:"今天"和"清除"(必填的字段没有"清除")。 - 打开时焦点落在月历里选中的那一天(没选时是今天);选了就关,焦点回到触发按钮;
Esc、点外面也会关。 - 带
name时值随表单提交;放进Field后标签、帮助文字、错误说明自动关联。
已实现为 <DatePicker value onValueChange placeholder name variant size min max isDateDisabled format invalid disabled required>。
选一段#
起止两天。月历加 range,值从一个日期变成一对 [起, 止](和滑块一样:传一个值是一个,传两个是一段)。
| 项 | 值 |
|---|---|
| 两端 | 和选中格一样的填充反转:surface-inverse 底、ink-inverse 字,加粗 |
| 中间 | 墨色 10% 的浅带。日期格之间本来就没有间距,所以它是连续的一条 |
| 预览 | 起始日定了、结束日还没定时,起始日到指针(或键盘)所在那天之间是墨色 5% 的浅带 |
| 今天 | 短线照旧;压在两端的块上时是 accent-ink-inverse |
- 点两下。 第一下定起始日,第二下定结束日;后点的那天更早也行,会自动排成先后。同一天点两下是只有一天的一段。选完之后再点是重新开始。
- 一段不只靠颜色:两端是实心的块,中间是连着的带子,形状上就是"从这到那"。浅带用的是墨色的不透明度,跟着主题走——暗色面板上它是比面板亮一档的带子。
- 高对比模式下两端是
Highlight的实心块;中间的浅带没了,换成上下各一条 2px 的Highlight线——日期格之间没有间距,连起来还是一条带子。还没定结束日时的预览带没有另画:这时候有实心的起始日和键盘焦点所在的那一格。见 色彩。(推断) - 起始日定了之后按
Esc取消这次选择,回到原来的那一段。 - 两端必须是可选的日子;不可选的日子可以被夹在中间。 "周末不发车"的日历上选周五到下周一是合理的。要禁止的话在
onValueChange里自己判。 - 读屏:范围里的每一天都带
aria-selected,两端的名称后面加"起始日""结束日";起始日定了之后有一句隐藏的播报:"再选结束日"。键盘和选一天时完全一样。
<DateRangePicker> 是它的字段形态,属性比照日期选择:
- 显示成
2026.10.03 – 2026.10.12;起止是同一天时只写一个日期。 - 结束日选完面板才关,焦点回到触发按钮。面板底下左边是一句提示("先选起始日,再选结束日"),右边是"清除"(必填时没有);没有"今天"——对一段来说它没有意义。
- 表单字段名是两个:
startName和endName。 - 外框、气泡面板、日历图标这层壳和日期选择是同一份。
没有做的:
- 双月并排。 两个月在窄屏放不下,要另写一套键盘和响应式的逻辑。一个月的网格本来就带着相邻月份的头尾几天,跨月的短区间直接点得到;长的翻一次月。
- 快捷区间(近 7 天、本月)。放在字段旁边用几个 筛选胶囊 或一个下拉更合适,值照样传给
value。 - 带时间;限制最短、最长天数。
文件上传#
选文件,或者把文件拖进来。(推断:虚线框是 空状态 的——"这里现在是空的";底是输入框的凹陷底;拖到上面时四角的 取景角 是"正在瞄准";文件行是组合框多选那种"已经定下来的值"的直角块,前面的扩展名是一个 直角标签)
它只管"选"和"列",不管"传"。 发请求、算进度是使用方的事;控件收文件、按类型和大小把关、把选好的列出来,每一行能显示使用方给的进度和结果。
投放区#
| 项 | md(默认) |
sm |
|---|---|---|
| 高 | 两行,上下内边距 16px | 一行,40px |
| 底与线 | surface-sunken 底,1px line-strong 虚线,直角 |
同左 |
| 内容 | 左对齐:一个 24px 的上传图标;一行字("把文件拖到这里,或者 选择文件",后四个字带下划线);一行说明(description,text-sm、ink-secondary) |
图标 20px,字和说明排成一行 |
| 状态 | 画法 |
|---|---|
| 悬停 | 线变 ink-secondary |
| 键盘聚焦 | 2px 的焦点环画在投放区上 |
| 文件拖到上面 | 线变成实线、ink;四角出现取景角;字换成"松开,放进来"。三样一起变,不只靠颜色 |
| 错误 | 线变 danger(仍是虚线) |
| 禁用 | 线 line、字 ink-disabled;拖进来不收 |
- 里面是一个真的
<input type="file">,看不见,铺满整个投放区:点哪儿都是打开系统的文件框,键盘聚焦后按空格或回车也是。所以它的名称、必填、随表单提交都是原生的。 - 选的、拖进来的、删掉之后剩下的,都会写回这个输入框——表单提交的就是下面列出来的那几个。
- 说明里把限制写清楚:"PDF 或图片,单个不超过 10 MB,最多 5 个"。这句话不是装饰,是让人少碰一次壁。
把关#
accept(扩展名和 MIME,image/* 这种通配也认)、maxSize(字节)、maxFiles;文件夹不收;同一个文件(名字、大小、修改时间都一样)不重复加。不是 multiple 时新选的替掉原来的。
没收的不会静悄悄地消失:投放区下面出一行——错误色的菱形图标加一句话,"2 个文件没有加进来:a.exe 类型不对;b.zip 超过 10 MB"——读屏会播报,同时 onReject 拿到每个文件和原因("type" / "size" / "count")。下一次加文件时这一行换掉。
文件行#
| 项 | 值 |
|---|---|
| 行 | 40px 高,直角,surface-muted 底;行与行之间留 4px |
| 扩展名 | 直角标签:surface-inverse 底、ink-inverse 字,font-tech、加粗、大写(PDF);没有扩展名时写 FILE |
| 文件名 | text-sm,过长截断,完整的名字在 title 里 |
| 大小 | font-tech、等宽、text-xs、ink-secondary:1.2 MB |
| 移除钮 | 右端 32px 的方钮,一个叉;名称是"移除某某" |
status |
画法 |
|---|---|
| 不传 | 只是列出来 |
"uploading" |
行底一条 4px 的 进度条,大小的位置换成百分比;progress 不传就是不确定进度 |
"done" |
大小后面一个成功图标(success),读屏多听到一句"已上传" |
"error" |
文件名下面一行 danger 的说明(error),前面是错误图标;传了 onRetry 就多一个"重试" |
新加进列表的那一行淡入(--duration-fast);一开始就列着的是静止的,只收一个时替上来的那一个也算新的。淡入写在列表的那一层上:renderFile 自己画的行同样有,单独拿去列"已有的附件"的 FileItem 没有。(推断,见 动效)
移除的那一行按高度收起并淡出(--duration-fast),连它占的行距一起收,收完才从页面里拿掉:下面的行和控件后面的内容是被带着上来的,不是跳上来的。焦点不等它(见下面"移除之后焦点有去处"),正在收起的那一行点不到、读屏读不到;它画的是自己最后的样子,不会为它再调一次 renderFile。只收一个时换上来的那一个是直接换的,旧的不收。(推断,见 动效)
已实现为 <FileUpload> 和 <FileItem>:
<Field label="交接附件" help="PDF 或图片,单个不超过 10 MB。">
<FileUpload
multiple
accept=".pdf,image/*"
maxSize={10 * 1024 * 1024}
maxFiles={5}
name="attachments"
value={files}
onValueChange={setFiles}
/>
</Field>- 值是
File[]。默认每个文件渲染一行不带状态的FileItem;选了就要传的,用renderFile(file, { remove })自己返回带status/progress的那一行。 FileItem单独导出:不经过投放区、只是列"已有的附件"时直接用它(name、size、href都是你给的)。- 移除之后焦点有去处:落到下一行的移除钮上,没有下一行就是上一行,一个都不剩就回到投放区。
- 放进
Field后标签、帮助文字、错误说明自动关联;点标签也是打开文件框。
没有做的:
- 发请求、分片、断点续传。
- 缩略图预览;从剪贴板粘贴;收整个文件夹。
表单布局#
- 标签在上、控件在下。不用左右对齐的标签列——它在窄屏和长标签下都不好用。
- 字段之间 20 – 24px;分组之间 40px 以上,或用一个小的分组标题隔开。
- 一行一个字段;只有关系紧密的短字段(如起止日期)才并排。
- 必填标记:标签后加一个强调色的小菱形或星号,并在表单开头说明其含义。实现里菱形用
accent-ink而不是action:行动色在亮色表面上只有 1.1:1,看不见。 - 提交区:主要按钮在右(或通宽),
control或action;取消用light或text。 - 整表的错误汇总放在表单顶部的 提示条 里,并逐项链接到对应字段。
行为与无障碍#
- 每个控件都有关联的
<label>。Field自动把标签、帮助文字、错误说明和里面的控件关联起来。 - 一组控件(几个复选、一个单选组)用
<Field group>:它渲染成<fieldset>+<legend>,禁用时里面的控件一起禁用。 - 错误信息用
aria-describedby关联到控件,并设aria-invalid="true"。错误说明排在帮助文字之前,两者同时保留。 - 错误说明要写"哪里不对、怎么改",不只是"格式错误"。
- 错误说明出现时按高度把下面的内容顶开并淡入,撤掉时收回去、再从页面里拿掉(进出都是
--duration-fast):下面的字段是被推着走的,不是被"啪"地顶一下。关联和错误态不等动效——aria-describedby、aria-invalid在error有了、没了的那一刻就变;出现时整句话从第一帧就在页面里,正在收起的那一句对读屏隐藏。没有错误时没有占位;一上来就带着错误的字段载入时是静止的。(推断,见 动效) - 不要只用颜色表示错误:加图标和文字。
- 不要用占位文字代替标签。
- 下拉、日期等复合控件用成熟的无障碍基元实现(下拉选择用的是 Base UI 的 Select)。
- 自动完成、输入类型(
inputmode、autocomplete)按字段设置。
宜 / 忌#
宜
- 保持表单安静:中性色,状态只在需要时出现。
- 用底边线的颜色变化表达状态。
- 把黄色限制在"已选"的记号上。
忌
- 圆角输入框配彩色发光的聚焦态。(账号 SDK 的输入框是白底胶囊:1.2px 的
#EEEEEE内描边,悬停和聚焦换成 1.6px 的#262626,出错#F96F68;不发光,但形状不取,见 账号 SDK 弹窗。) - 给输入框切角——它需要完整的边界。
- 整条黄色的开关轨道、整块黄色的复选框。
- 在表单页上加网格、斜纹、巨字等装饰。
- 方形和菱形两套符号混用。
来源#
- 语言下拉、双标签开关:measurements.md 第 8.12、8.14 节。
- 菱形表单符号:VBeatDead/ReEnd-Components。
- 云游戏页"功能设置"的开关:measurements-cloud.md 第 9.3 节。
- 账号 SDK 的输入框、勾选框、设备验证格(只作参照,形状不取):measurements-cloud.md 第 11.6、11.7 节;对照见 云游戏页。
- 其余为本规范的推断。