交互与体验设计指南
💡 本文是人类阅读版(决策时读、理解 why)。对应的写代码护栏在开发规范的
UX-001~UX-010(已 sync 进项目CLAUDE.md,AI 写代码时强制遵守)。两者同主题、两层:这里讲透「为什么」,护栏给「照着做」。
交互细节统一,是「专业感」最廉价、也最有效的来源。用户不会逐条说出「这个按钮 hover 怎么和那个不一样」,但会模糊地觉得「这站做得糙、不太可信」。本文把 swift-web 推荐的交互体验基线讲清楚,并给出在 framework 组件上的落地方式。
为什么一致性 = 专业感
用户在产品里会建立预期:hover 一个红按钮看到它变暗,大脑就默认「所有红按钮 hover 都变暗」。这叫可预测性(predictability)。
一旦同类元素表现不一(这里变亮、那里变暗),用户每遇到一次「咦怎么不一样」,就要多花一次**认知(cognitive load)**去重新理解。单次微不足道,但全站累积起来,体感就是「不精致」。反过来,严格一致让界面「隐形」—— 用户不用思考交互本身,注意力全留给内容。
这正是 Nielsen 可用性十大启发式里**「Consistency and standards(一致性与标准)」**的核心。下面四组就是把这条原则落到 swift-web 的具体场景。
一、反馈与状态
1. 交互反馈一致性:同类元素同表现(UX-001)
同类交互元素(按钮 / 链接 / 卡片)的 hover / active / focus 反馈必须全站一致。反馈方式按填充类型分三档:
| 填充类型 | hover 反馈 | 为什么 |
|---|---|---|
| 实色填充(品牌色 / 浅色 / accent / 彩色图标) | 变暗 brightness(0.92)(active 0.85) | 绝大多数按钮;变暗在实色上永远可感知 |
| 深色 / 半透明深色填充(深背景上) | 变亮 brightness(1.12) | 深色上再变暗会沉进背景看不见,需变亮 |
| 半透明纯白 ghost | 提高背景透明度(如 12% → 22%) | 纯白已到最大值,brightness 推不动它 |
最容易踩的两个坑:
/* ❌ 同一组品牌红 CTA,登录页变暗、付费墙变亮 —— 同种按钮两种行为,用户察觉「不一样」 */
.login-submit:hover { filter: brightness(0.92); } /* 变暗 */
.paywall-cta:hover { filter: brightness(1.08); } /* 变亮,不一致 */
/* ❌ 半透明白底按钮想靠 brightness 提亮 —— 纯白(255)乘任何倍数仍被 clamp 回 255,hover 毫无反馈 */
.ghost-btn { background: rgb(255 255 255 / 12%); }
.ghost-btn:hover { filter: brightness(1.08); } /* 零效果 */
/* ✅ 按填充类型对档,全站统一 */
.cta:hover { filter: brightness(0.92); }
.cta:active { filter: brightness(0.85); }
.dark-btn:hover { filter: brightness(1.12); }
.ghost-btn { background: rgb(255 255 255 / 12%); transition: background 0.15s; }
.ghost-btn:hover { background: rgb(255 255 255 / 22%); }
这条规则的由来:一次真实的付费墙开发里,品牌红 CTA 在付费墙写成了变亮、而全站其它地方都是变暗 —— 同一种红按钮两种 hover。统一回变暗后,「专业感」立刻回来了。CSS 写法(
filter: brightnessvs 背景透明度)的完整说明见样式与 Token。
2. 异步操作必须锁住入口(UX-002)
任何异步操作(提交 / 上传 / 拉取 / 删除)触发的可点元素,在 async 进行期间必须禁用或显 loading,否则用户能反复点 → double-submit、状态错乱、重复请求。
用 SwButton 时配 :loading 即可,组件会自动禁用并显示加载态:
<SwButton :loading="submitting" @click="onSubmit">提交</SwButton>
<script setup>
const submitting = ref(false)
async function onSubmit() {
submitting.value = true
try {
await api.submit()
} finally {
submitting.value = false
}
}
</script>
非按钮的可点元素(卡片点击、自定义触发)同理 —— pending 期把入口锁住。
3. 数据驱动视图必须备齐四态(UX-003)
「拉数据再渲染」的视图(列表 / 详情 / feed)只画成功态,会在另外三态翻车:加载时白屏、空数据一片空白让人以为坏了、请求失败卡死没重试。四态都要画:
- loading → 骨架屏(优先,与真实内容等尺寸,顺带防 CLS)或 spinner,别白屏
- empty → 明确空状态文案 + 下一步引导(「还没有内容,去逛逛」+ 入口),别只留空白
- error → 错误说明 + 重试入口,别卡死或静默
- success → 正常渲染
framework 多个组件已内建四态钩子,优先用:SwGallery(loading / error / finished)、SwVideoFeed(finished + 内置逐条加载 / 缓冲指示器,卡顿反馈无需业务自己画)。
<template>
<SkeletonList v-if="pending" />
<ErrorState v-else-if="error" @retry="refresh" />
<EmptyState v-else-if="!list.length" />
<ContentList v-else :items="list" />
</template>
二、无障碍与可操作
无障碍不是「给少数人的额外功能」—— 键盘用户、读屏用户、前庭敏感用户都是真实用户,下面几条还直接对应 WCAG 标准条款。
1. 尊重 prefers-reduced-motion(UX-004)
位移 / 缩放 / 视差 / 自动轮播 / 大幅过渡等动效,对前庭功能障碍用户会引发眩晕、恶心(WCAG 2.3.3 Animation from Interactions)。非必要动效必须在 @media (prefers-reduced-motion: reduce) 下关闭或降级:
.card { transition: transform 0.3s; }
.card:hover { transform: scale(1.05) translateY(-8px); }
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover { transform: none; } /* 或仅留极轻微 opacity 反馈 */
}
必要的状态反馈(loading 转圈等)可保留,但应克制。
2. 弹层必须做焦点管理(UX-005)
模态弹层(modal / dialog / 抽屉)若不管焦点 —— 打开后焦点仍在背景、键盘 Tab 跑到被遮挡内容、关闭后焦点丢失 —— 键盘和读屏用户就完全无法操作。合规要求:打开时焦点移入弹层、内部 Tab 不逃逸(focus trap)、Esc 可关、关闭时焦点还原到触发元素。
好消息:SwModal / useModal() 基于 reka-ui,这些开箱即合规。所以——用框架弹层就对了,别自己写 div overlay(自建必须自己实现 trap + restore,很容易漏)。
3. 触摸热区达标(≠ 撑大视觉)+ focus 指示不 reflow(UX-006)
- 要约束的是「热区」,不是视觉尺寸:移动端点击区太小会频繁误触,但规范要求的是可点区域 —— WCAG 对 target 的定义是「可接受指针操作的区域」,Material Design 3 也明确热区「extending beyond the visual bounds of an element」。合规基线是 WCAG 2.5.8 AA 的 24×24(44×44 出自 AAA 的 2.5.5),且 inline 文本链接明文豁免(正文内链 / 面包屑 / 页脚链接不受最小尺寸约束)。
- framework 不做全局
min-height兜底(framework#265 移除;曾做过,导致图标钮被撑大、面包屑错位)。视觉小于 44px 的紧凑控件加sw-hit-areaclass 主动扩热区、视觉不变;framework 自带紧凑图标钮已在组件内部挂好,用Sw*组件无需触摸端补丁。见样式与 Token · 响应式工具类。 - focus 指示不 reflow:focus 视觉指示不要靠新增 / 加宽 border 实现(会改盒模型尺寸、挤动布局)。已有 border 的改
border-color,无 border 的用outline或box-shadowring。
两条的 CSS 细节见样式与 Token。
三、导航与确认
1. 破坏性操作必须二次确认(UX-007)
删除 / 清空 / 退出登录 / 不可逆扣费等破坏性操作,点击即执行 = 误触一次就造成无法挽回的损失。
- 高风险 / 不可逆 → 执行前二次确认,用
useModal()确认框(别用原生confirm():样式不可控、阻塞主线程、移动端体验差):async function onDelete() { const ok = await useModal().open({ title: '确认注销?', description: '此操作不可恢复' }) if (ok) await api.deleteAccount() } - 低风险 / 可撤销(归档、移除单项)→ 可用「先执行 + 撤销 toast」替代硬确认,少打断用户。
2. 反馈渠道按场景选(UX-008)
反馈渠道用错场景会让反馈失效 —— 把需要决策的内容塞进一闪而过的 toast、或用 toast 报字段校验错(用户来不及看)。按场景选:
| 场景 | 用 | 为什么 |
|---|---|---|
| 成功 / 轻量提示(非阻断) | toast —— useToast() | 短暂、不打断、自动消失 |
| 需用户决策 / 确认 | modal —— useModal() | 阻断、需主动响应、不会错过 |
| 字段级校验错误 | inline 就近显示 —— SwFormField | 错在哪显在哪、可反复看 |
原生 alert() 一律不用(样式不可控、阻塞)。
// ❌ 表单校验失败弹 toast —— 一闪而过,用户没看清就消失
useToast().error('手机号格式不对')
// ✅ 字段错就近 inline,提交成功才 toast
setFieldError('phone', '手机号格式不对')
// ...提交成功后:
useToast().success('已保存')
四、感知性能
「感知性能」关心的是用户感觉快不快,不只是真实加载时间。
1. 防布局跳动(CLS)(UX-009)
图片 / 广告 / 懒加载内容 / web 字体到达时挤动已渲染内容(Cumulative Layout Shift)—— 用户正要点的按钮被推走、阅读位置乱跳,体感很差,移动端 Lighthouse 也明显掉分。
核心做法:任何异步插入的内容都预留占位尺寸。图片用 SwImage 传 ratio(组件用 aspect-ratio 撑住容器);广告位给固定高;骨架屏与真实内容等尺寸。
<!-- ratio 撑住高度,图片加载前后容器不跳 -->
<SwImage :src="cover" alt="封面" ratio="16/9" />
2. 乐观更新(opt-in)(UX-010)
点赞 / 收藏 / 关注等低风险高频操作,如果每次都等网络往返才变 UI,体感会迟钝。可以乐观更新:先改 UI 再发请求,失败回滚 + 提示。
// ✅ 点赞乐观 + 失败回滚
liked.value = true
try {
await api.like(id)
} catch {
liked.value = false
useToast().error('操作失败,请重试')
}
边界(重要):只有低风险、可回滚的操作适用。有副作用 / 不可逆 / 涉及金额的操作(支付、删除、发布、提交订单)绝不要乐观 —— 必须等服务端确认,否则「乐观显示成功、实际失败」会误导用户、酿成事故。
业界理论速览
本文只讲 swift-web 的可执行落地。想深入理解通用 UX / 无障碍理论,看下面这些权威源(本站不复制其内容,避免过时):
- Nielsen Norman Group — 10 Usability Heuristics —— 一致性 / 反馈 / 防错等十大通用启发式
- WCAG 2.2 —— Web 无障碍标准(焦点 / 对比度 / reduced-motion 等条款的权威定义)
- Apple Human Interface Guidelines —— 触摸目标 / 移动端交互
- Material Design 3 —— 状态层 / 动效 / 反馈的系统化参考
与开发护栏的关系
本文(人类长文)与开发规范里的 UX-001~UX-010(AI 护栏 / MR review 检查项)是同一套原则的两层:
- 写代码、过 review → 以护栏(项目
CLAUDE.md的「交互与体验」段)为准,每条带反例 + 兜底标注。 - 理解为什么、做设计决策 → 读本文,展开的 why + 范例 + 业界理论都在这。
两层互相链接、不互抄;护栏更新时本文手工同步。