Yeti 表单控件 Field 组件完全指南:标签、提示与自显现错误的状态驱动表单
【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yeti
Yeti 是一个 CSS-first、原生、零构建的布局与样式框架(项目说明),而field是其中负责"一个表单控件及其附属信息"的组件:它把一个 label、一个原生控件、一段可选 hint 提示和一段错误信息捆绑为一个整体。本文以 docs/field.md 为骨架,结合 field.css 源码、manifest.json 声明与 field.spec.js 测试,完整讲解 field 的用法、属性、子元素、设计令牌与无障碍细节,让你能直接用纯 HTML + 类名搭建无需 JavaScript 的校验型表单。
Field 是什么:一个控件的一切
Yeti 的 Field 组件定义非常凝练:一个表单控件(text、email、number、select、textarea、checkbox、radio、switch、range 等任意一种),配上它的标签、可选的帮助文本,以及一个"当控件无效时自己显现出来"的错误信息。
它解决的是裸 HTML 表单做不到的三件事:
- label 与控件的关联(
for↔id配对); - 帮助文本(hint)的排版与语义挂载;
- 错误信息的显示时机——既不在页面加载时全红一片,也不需要一行脚本。
在 Yeti 的实践里,一个表单通常是stack布局下若干 field 与一个 button 的纵向组合;field 负责承接 HTML 语义本身无法表达的那部分。
最小示例:一个带提示与错误的邮箱输入
官方文档给出如下最典型的结构(example.html 中完全一致):
<div class="field"> <label for="email">Email</label> <input id="email" type="email" required aria-describedby="email-hint email-error"> <p id="email-hint">.field { display: flex; flex-direction: column; gap: var(--yeti-field-gap); } .field > * { margin: 0; } .field > :is(label, legend) { font-weight: var(--yeti-weight-strong); font-size: var(--_yeti-size-text); }label 与 legend 使用粗体(--yeti-weight-strong),字号跟随内部令牌--_yeti-size-text(由data-size决定)。
2. 控件读取"控件令牌",主题一处改动处处生效
文本类控件(input 的非特殊类型、select、textarea)直接以原生元素配合令牌样式:
.field :is(input:not(:where([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"])), select, textarea) { min-block-size: calc(var(--yeti-control-size) + max(0px, var(--_yeti-size-space) - var(--yeti-space-sm))); padding: 0 var(--_yeti-size-space); font: inherit; font-size: var(--_yeti-size-text); color: var(--yeti-color-text); background-color: var(--yeti-control-surface); border: var(--yeti-border-width) solid var(--yeti-control-border); border-radius: var(--yeti-control-radius); transition: border-color var(--yeti-duration-fast) var(--yeti-ease); }这正是文档强调的:控件由--yeti-control-*一组令牌统一样式,主题只要改了--yeti-control-radius,所有输入框的圆角随之改变。例如 soft.css 主题正是通过--yeti-control-radius: var(--yeti-radius-md);一行让控件更圆润。
特殊处理还包括:
textarea额外设置padding-block与min-block-size: 4lh(约 4 行文本高度,这里用到了lh单位,见"浏览器支持"一节);select使用appearance: none去掉原生外观,把背景替换为--yeti-control-chevron这个内联 SVG 箭头图像,并预留右侧 padding 防止文字压到箭头;:focus-visible时边框变为--yeti-color-border-strong,键盘焦点清晰可辨。
3. 错误显现的时机:::user-invalid与aria-invalid
错误默认隐藏,仅在两种情况下显现:
.field > [data-error] { display: none; ... } .field:has(:user-invalid, [aria-invalid="true"]) > [data-error] { display: block; }- 客户端校验:控件
:user-invalid命中——即用户已经"触碰"过控件且值无效(email输入nope后失焦即触发),错误显现; - 服务端往返:服务器返回后你设置
aria-invalid="true",错误立即显现,无需用户再操作。
同时,无效控件自身的边框会转为告警色(--yeti-color-alert),且只红自己的控件:
.field:has(> :user-invalid, > [aria-invalid="true"], > .affix :is(:user-invalid, [aria-invalid="true"])) > :is(input, select, textarea), .field:has(> .affix :is(:user-invalid, [aria-invalid="true"])) > .affix > :is(input, select) { border-color: var(--yeti-color-alert); }测试 field.spec.js 验证了这套行为:#email-error初始display: none;填入nope并 blur 后变为block且边框色改变;而#name-error(夹具中通过aria-invalid="true"模拟服务端错误)在页面打开时就直接显示。另一个用例专门验证"一个无效控件只红自己的控件"——给#p-a设aria-invalid="true"后,兄弟#p-b的边框保持不变。
4. required 的视觉标记
.field:has([required]) > :is(label, legend)::after { content: " *"; color: var(--yeti-color-alert-text); }required控件会在 label(或 legend)后追加一个星号标记,颜色为告警文字色。测试通过读取getComputedStyle(..., '::after').content确认它包含*。注意:星号只是装饰,真正被辅助技术朗读的是required属性本身(见"无障碍"一节)。
5. checkbox 与 radio:自动内联 + 变体色勾选
checkbox 与 radio 使用appearance: none重绘,尺寸为1.25em,勾选时:
.field input:is([type="checkbox"], [type="radio"]):checked { background-color: var(--_yeti-variant); border-color: var(--_yeti-variant); box-shadow: inset 0 0 0 0.2em var(--yeti-control-surface); }即变体色实心中心 + 表面色内环的视觉效果(radio 圆角为 50%)。测试会验证勾选后背景色等于--_yeti-variant解析出的颜色、取消勾选后背景回到--yeti-control-surface。
布局上,checkbox/radio 所在的 field自动变成内联行(label 在控件右侧),不需要任何属性:
.field:is([data-inline], :has(> input:is([type="checkbox"], [type="radio"]))) { flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--yeti-space-xs); }测试用几何断言确认了"label 左侧大于控件右侧、二者垂直居中"。内联时 hint 与 error 通过flex-basis: 100%换行独占一行。
6. fieldset 分组:多个控件共享一个 legend
当需要给一组选项(如一组 radio)命名时,把field类直接用在fieldset上:
<fieldset class="field"> <legend>Notify me by</legend> <div class="field"><input id="n-email" type="checkbox" name="notify" value="email"><label for="n-email">Email</label></div> <div class="field"><input id="n-sms" type="checkbox" name="notify" value="sms"><label for="n-sms">Text message</label></div> <p>fieldset.field { padding: var(--yeti-space-sm) var(--yeti-space-md); border: var(--yeti-border-width) solid var(--yeti-control-border); border-radius: var(--yeti-control-radius); } fieldset.field > legend { padding-inline: var(--yeti-space-xs); }7. switch:一个 checkbox 加上role="switch"
checkbox 加role="switch"就变成开关:轨道 + 滑块,打开时滑块滑到末端且轨道取变体色:
<div class="field"><input id="dark" type="checkbox" role="switch"><label for="dark">Dark mode</label></div>源码实现是inline-size: 2.25em的胶囊轨道(--yeti-radius-full),用radial-gradient画圆形滑块,background-position从0% 50%移到100% 50%完成滑动,勾选时背景变体色、滑块环消失(box-shadow: none)。测试断言了轨道宽度大于高度的 1.5 倍、切换前后背景色与位置均变化。
8. range:细轨道 + 圆滑块
<div class="field"><label for="volume">Volume</label><input id="volume" type="range" min="0" max="100" value="40"></div>range 输入本身是一个控件的完整高度(block-size: var(--yeti-control-size)),保证滑块是一个舒适的点击目标;轨道只有0.25em高、圆角胶囊状、颜色为--yeti-control-border;滑块为 1.25em 圆形、变体色带边框。CSS 同时书写::-webkit-slider-*与::-moz-range-*两套伪元素。测试指出轨道"不会填充到当前值"——因为 CSS 读不到控件的值,这是文档明确说明的设计取舍。
无障碍(Accessibility)
文档对无障碍给出明确要求,且这些要求被 validator 与测试双重约束:
- label 的
for必须匹配控件的id,Yeti 的 validator 会拒绝缺少配对的示例; - 把hint 与 error 的 id 放进控件的
aria-describedby,屏幕阅读器就能随控件朗读帮助文本,并在错误出现的瞬间朗读错误; - 服务端发现的错误用
aria-invalid="true"呈现; - required 标记只是装饰,辅助技术真正读到的是
required属性本身; - switch 就是带
role="switch"的 checkbox,它的 label 即开关的名称; - range 和其他控件一样需要 label,当数值不是人能直接说出的内容时,应提供
aria-valuetext。
这些行为在 manifest.json 的a11y.notes中原文记录,并在 field.spec.js 中通过 axe 无障碍扫描("has no accessibility violations")与明暗两种配色方案下的 AA 对比度测试("text meets AA in light/dark")得到验证。
属性(Attributes)
| 属性 | 类型 | 取值 | 默认 | 说明 |
|---|---|---|---|---|
data-size | enum | sm,md,lg | md | 缩放控件的高度与文字。 |
data-inline | boolean | — | — | 把 label 放到控件旁边。checkbox 与 radio 不需要它本身就内联。 |
data-variant | enum | primary,secondary,success,warning,alert,neutral | primary | 勾选后的 checkbox / radio 的颜色。 |
属性背后的机制
从源码结构看,这些属性不是 field 特有的规则,而是复用 attributes.css 中一套通用的"布局属性"解析:
data-variant为元素挂载--_yeti-variant等一组变体色阶梯变量,取值集合定义在 vocabulary.json 的"variant"词条中;field 通过:not([data-variant])设置默认的 primary 变体,所以不会继承父元素的变体;data-size解析为两个内部令牌--_yeti-size-text(文字步进)与--_yeti-size-space(间距步进),例如lg对应--yeti-text-lg与--yeti-space-md;sm则更紧凑。field.css 中控件的min-block-size与 padding 都读取这两个令牌,从而整体缩放;data-inline是布尔属性,仅用于把 label 排到控件旁边(对 checkbox/radio 属冗余写法,因为:has()已自动处理)。
size-control词条位于 vocabulary.json 中,值为["sm", "md", "lg"]。
子元素(Children)
manifest.json以> selector形式声明了 field 允许的子元素结构(每个最多一个):
> label:0~1 个。label,for指向控件的 id;除非是带 legend 的 fieldset,否则必须存在。> legend:0~1 个。当 field 是分组多个控件的 fieldset 时使用。> input:0~1 个。控件。> select:0~1 个。控件。> textarea:0~1 个。控件。> .affix:0~1 个。控件槽位交给 affix 组件——即"带附属物的控件"或"两个控件拼接"(参见 affix 组件 与 affix.css)。> [data-hint]:0~1 个。帮助文本,由控件的aria-describedby引用。> [data-error]:0~1 个。错误消息,控件无效前保持隐藏。
hint 与 error 的样式在源码中分别为--yeti-text-sm字号,hint 用--yeti-color-text-muted弱化,error 用--yeti-color-alert-text告警色。
设计令牌(Tokens)
| 令牌 | 说明 |
|---|---|
--yeti-field-gap | label、控件与 hint 之间的间距。 |
--yeti-control-size | 控件的最小高度。 |
--yeti-control-radius | 控件的圆角。 |
--yeti-control-border | 控件静止时的边框。 |
--yeti-control-surface | 控件的背景。 |
--yeti-control-chevron | select 的箭头图像。 |
--yeti-color-alert | 无效控件的边框颜色。 |
默认值与主题化
这些令牌的默认值定义在 surface.css 与 components.css(并汇总于 tokens.json 的control分组):
--yeti-control-size: 2.5rem(md尺寸下按钮或输入的最小块高);--yeti-control-radius: var(--yeti-radius-sm);--yeti-control-border: var(--yeti-color-border);--yeti-control-surface: var(--yeti-color-surface);--yeti-control-chevron:一个中灰色内联 SVG 箭头(主题可为深色控件表面提供更亮的箭头,如 tokens.json 中的说明);--yeti-field-gap: var(--yeti-space-xs)。
--yeti-color-alert定义于 color.css,是一个随明暗模式切换的light-dark()颜色(浅色下约#af3c3a)。正如文档所说,主题只要覆盖--yeti-control-radius(如 soft.css 改为radius-md)即可让全部控件变圆润——这正是"控件读取控制令牌"设计带来的杠杆效应。
内部令牌(可能在次要版本间变化)
以下令牌以下划线开头,是内部实现细节,不建议在主题中依赖:
--_yeti-variant--_yeti-on-variant--_yeti-size-text--_yeti-size-space
浏览器支持
- 无守卫直接使用:
:has()、:user-invalid、appearance: none、lh单位; @supports守卫之后:无。
这意味着 field 依赖现代浏览器的原生能力(:user-invalid是 Chromium 111 / Firefox 88 / Safari 16.5+ 支持的伪类),但换来的是零脚本的校验体验。若目标浏览器较旧,需要自行评估降级方案。
JavaScript
无。该组件纯 CSS 实现(CSS only),自 7.0.0 版本起可用。错误显隐、开关滑块、range 滑块全部由 CSS 状态与伪元素完成,这也是 field 在 field.spec.js 中能通过纯浏览器断言验证(无 mock、无注入脚本)的原因。
小结
Yeti 的 field 组件展示了"CSS-first"表单设计的完整路径:用:has()+:user-invalid把校验状态从控件本身映射到错误文本与边框,用appearance: none重绘 checkbox、radio、switch、range,用控制令牌统一所有控件的尺寸与圆角,再以 validator 强制 label↔id 配对保证无障碍底线。按本文的示例与属性说明,你可以直接用语义化 HTML 搭建出无脚本、可访问、可主题化的完整表单。
【免费下载链接】yetiA CSS-first, native, zero-build layout and styling framework for web designers.项目地址: https://gitcode.com/gh_mirrors/fo/yeti
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考