三种布局解决表单对齐与校验:shadcn-svelte Field 组件实战
【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte
表单一多,排版就开始失控
表单只有三四个字段时没人会在意对齐。等到变成十几个字段,问题就来了:label 该放控件上方还是左侧?错误提示到底跟字段还是跟表单?屏幕变宽之后,要不要为移动端和桌面端各写一套样式?如果你被设置页折腾过,应该都中过招。shadcn-svelte 的 Field 组件族就是为这个场景做的:一组组件同时管对齐、校验展示和响应式切换,垂直、水平、响应式三种布局共用同一套标签。
组件拆解:谁负责什么
源码目录docs/src/lib/registry/ui/field/下有 11 个文件,由index.ts统一导出。按交互职责分,边界就清楚了:
- 布局骨架类:
Field与Field.Content。前者是单字段的容器,负责决定标签、控件、辅助文本朝哪个方向排;后者是一个纵向 flex 列,专门把标签和描述文字"打包"成一列,解决水平布局里描述文字错位的问题。 - 文本标注类:
Label、Title、Description。Label 是真正的表单标签,靠for关联控件;Title 只是标题样式的div,不承担表单关联,专为选择卡片场景存在;Description 是辅助说明。 - 分组语义类:
Set、Legend、Group。前两者渲染原生fieldset/legend,承担语义分组;Group 是视觉堆叠容器,顺带声明了容器查询。 - 状态反馈类:
Error、Separator。Error 负责错误消息且自适应单条/多条;Separator 是纯视觉分隔线,没有任何 ARIA 语义。
| 子组件 | 渲染元素 | 核心 Props | 一句话职责 |
|---|---|---|---|
| Field | div(role="group") | orientation | 单字段包装器,决定纵向/水平/响应式 |
| Field.Set | fieldset | - | 语义分组容器 |
| Field.Legend | legend | - | 分组标题,归属 fieldset |
| Field.Group | div | - | 堆叠多个字段,自带 @container/field-group |
| Field.Content | div | - | 纵向列,打包标签与描述 |
| Field.Label | label | for | 字段标签,按 id 关联控件 |
| Field.Description | p | - | 可选辅助说明文本 |
| Field.Error | div(role="alert") | errors | 错误消息,单条多条自适应 |
Title 与 Separator 两个子组件参见源码目录。它们之间的嵌套关系是固定的:
Field.Set(fieldset)Field.Legend分组标题Field.Description组级说明Field.Group(容器查询宿主)Field.Field(role="group")Field.ContentField.LabelField.Description
- 控件:Input / Select / Switch
Field.Error
Field.Separator
快速跑通:最小可运行示例
安装有两条路:CLI 执行npx shadcn-svelte@latest add field一条命令到位;手动则把docs/src/lib/registry/ui/field/整个目录复制到项目的$lib/components/ui/field/,前提是$lib/utils.js里已有cn工具函数。
下面的最小示例一次覆盖三种形态:正常字段、错误字段、水平布局字段。跑通后你应该能直观看到 Field 家族的三种基本用法。
<script lang="ts"> import * as Field from "$lib/components/ui/field/index.js"; import { Input } from "$lib/components/ui/input/index.js"; import { Switch } from "$lib/components/ui/switch/index.js"; </script> <Field.Group> <Field.Field> <Field.Label for="name">Full name</Field.Label> <Input id="name" placeholder="Evil Rabbit" /> </Field.Field> <Field.Field><Field.Field> <Field.Label for="email">Email</Field.Label> <Input id="email" /> <Field.Description>Used to receive reset links.</Field.Description> </Field.Field>水平并排:桌面端的对齐细节
orientation="horizontal"切换为 flex-row 并垂直居中,但源码里藏了两个细节(docs/src/lib/registry/ui/field/field.svelte#L9-L12):当子元素中存在Field.Content时改为顶部对齐(items-start),否则描述文字会跟着垂直居中立不正;复选框和单选框额外加mt-px,把圆点顶到标签基线上。所以水平布局建议始终用Field.Content包裹标签与描述。
<Field.Field orientation="horizontal"> <Field.Content> <Field.Label for="email">Email</Field.Label> <Field.Description>Used to receive reset links.</Field.Description> </Field.Content> <Input id="email" /> </Field.Field>标签短、字段多时用它省纵向空间;描述文字会折多行时别用,顶部对齐的错位感会比较明显。
容器查询驱动的响应式切换
Field.Group自身声明了@container/field-group(docs/src/lib/registry/ui/field/field-group.svelte#L13-L22),把自己注册为容器查询的宿主。orientation="responsive"的所有切换都发生在@md/field-group这个容器断点:低于断点走纵向,高于断点走水平。关键在于它读取的是Field.Group的宽度而不是视口宽度——表单嵌在窄侧边栏里就保持纵向,放在宽卡片里就自动变横向,不用写任何媒体查询。
<Field.Group> <Field.Field orientation="responsive"> <Field.Content> <Field.Label for="name">Name</Field.Label> <Field.Description>Your full name.</Field.Description> </Field.Content> <Input id="name" required /> </Field.Field> </Field.Group>嵌入侧边栏、抽屉、卡片里的表单一律用 responsive;整页大表单则直接用 horizontal 更可控。
错误状态与校验链路
错误状态是三层标记各管一段,缺一不可:
- 视觉层:
<Field.Field><Field.Field><Field.Error errors={data?.fieldErrors?.email} />无障碍设计细节
- fieldset/legend 分组:场景是十几个复选框共用一个"通知"标题;行为是 Set 渲染原生 fieldset、Legend 渲染原生 legend,屏幕阅读器把标题解析为整个组的名称,Tab 在组内连续移动;收益是用户不必在每个复选框上重听一遍标题。
- role="group" 命名继承:场景是选择卡片,卡片内没有可见的单字段标签;行为是 Field 输出
role="group"(docs/src/lib/registry/ui/field/field.svelte#L38-L47),嵌套在外层 Label 或 Legend 之内时,内部控件继承组级可访问名称;收益是阅读器能播报"计算环境,Kubernetes,未选中",卡片虽然看着没标签,实际不无名。 - 分割线保持非语义:场景是多个 Set 区块之间用 Separator 分隔;行为是 Separator 只是带背景的 div,不进朗读顺序;收益是屏幕阅读器跳过分割线,按区块连续朗读,边界清晰但不被打断。
进阶组合与常见误区
组合技巧
整卡可点选的选择卡片:把
Field包进Field.Label里,整张卡片就变成一个 label,点卡片任意位置都会勾选单选/复选框。官方示例docs/src/lib/registry/examples/field-choice-card.svelte的做法是 Label 包水平方向的 Field,Content 内放 Title 加 Description,右侧挂RadioGroup.Item:<Field.Label for="kubernetes"> <Field.Field orientation="horizontal"> <Field.Content> <Field.Title>Kubernetes</Field.Title> <Field.Description>Run GPU workloads on a K8s cluster.</Field.Description> </Field.Content> <RadioGroup.Item value="kubernetes" id="kubernetes" /> </Field.Field> </Field.Label>多 Set 加 Separator 堆叠:官方示例
docs/src/lib/registry/examples/field-field-group-demo.svelte用两个 Set 演示长设置页——每个 Set 内部再套一层 Group 堆叠复选框行,Set 之间只插一条 Separator:<Field.Group> <Field.Set> <Field.Label>Responses</Field.Label> <Field.Description>Get notified when requests take time.</Field.Description> </Field.Set> <Field.Separator /> <Field.Set> <Field.Label>Tasks</Field.Label> <Field.Description>Get notified when your tasks have updates.</Field.Description> </Field.Set> </Field.Group>Separator 内部基于 Separator 组件绝对定位一条贯穿线,如果传入子内容(比如文字标签),会用
bg-background背景色的内联块居中覆盖,做出"带文字的分割线"。容易踩的坑
- Label 的
for与控件id没对上:for 指向不存在的 id,标签就失去表单关联,点文字无法聚焦输入框;解法是把控件的 id 原样抄进 for。 - horizontal/responsive 下描述文字错位:描述文字直接挂在 Field 里而不进 Content,水平模式下它不会跟标签对齐;标签和描述永远一起包进
Field.Content。 - Separator 用太密:每个字段之间都插分割线,视觉上碎、朗读顺序里区块边界反而模糊;只在 Set 级别的区块边界保留。
- 只加 contenteditable="false">【免费下载链接】shadcn-svelteshadcn/ui, but for Svelte. ✨
项目地址: https://gitcode.com/GitHub_Trending/sh/shadcn-svelte
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考