关于 defaults
Defaults 是定义界面通用规范的技能,涵盖表面、导航、表单、加载状态、无障碍等常见细节。它为开发者提供具体检查项和实现指导,帮助不同屏幕保持一致行为。
主要功能
- 01表面、阴影与滚动规则
- 02焦点、键盘和触控指导
- 03表单输入与验证规范
- 04加载及更新反馈模式
- 05响应式与无障碍检查
适用场景
- 01审查界面通用默认规范
- 02检查表单和加载状态
- 03核对键盘、触控与焦点行为
- 04避免长内容引发的布局问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playful-design/ router --skill defaults安装后在 Claude Code 中输入/defaults确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSErecipes.mdagents/openai.yaml详细介绍
默认值
此技能包含适用于每个屏幕的界面默认值,涵盖表面、字体、声音、焦点、表单、加载和叠加层。
先用令牌,再用这些值
以下各项有对应值时,请使用项目的令牌。没有对应令牌时,这里的值就是精确值。硬切断滚动内容或阴影被裁切,在任何样式下都属于问题。
文字和标点属于 copy。使用项目的基础组件构建这些内容属于 reuse。是否应该存在声音或动画属于 restraint。元素的移动方式属于 motion,颜色和对比度属于 color。同心圆角、视觉微调和图标对齐属于 alignment。
滚动边缘渐隐
滚动容器的两端使用 mask-image 渐变实现渐隐。溢出边缘处硬切断属于 defaults-fade-not-cut。具体做法见 recipes.md。
阴影绝不裁切
为滚动容器留出足够的内边距,使子元素的阴影能完整显示在容器内。被 overflow: hidden 裁切的阴影属于 defaults-no-clipped-shadow。参见 recipes.md。
代码和表格横向滚动,绝不换行
代码块和数据表应横向滚动,并隐藏滚动条。代码行发生换行属于 defaults-scroll-not-wrap。参见 recipes.md。
叠加层保留滚动
可滚动的对话框、抽屉、菜单和侧面板应设置 overscroll-behavior: contain。列表滚动到底时导致其后的页面滚动,属于 defaults-contain-overscroll。
粘性界面元素保留边缘
粘性页眉或页脚应在滚动内容一侧设置 1px 边框。没有边框的粘性页眉属于 defaults-sticky-border。
将 scroll-padding-block-start 设置为粘性页眉的高度,确保锚点和获得焦点的字段显示在页眉下方。获得焦点的字段被页眉遮挡,属于 defaults-clear-sticky-chrome。
浮动表面使用环形描边,而非边框
浮动的弹出层、菜单或卡片应在阴影堆栈内配合一圈半透明的 1px 描边。阴影旁设置不透明的 border,属于 defaults-ring-not-border。在环形描边下保留 border: 1px solid transparent,以便强制颜色模式仍能显示边缘。
阴影应分层并朝一个方向投射
使用项目的 elevation 令牌。如果没有,则叠加三层阴影:模糊半径逐渐增大、不透明度逐渐降低,且全部向下偏移。只使用一个 0 4px 16px rgba(0, 0, 0, 0.2) 属于 defaults-layered-shadows。参见 recipes.md。
若要在悬停时抬高表面,应让承载抬高后阴影的伪元素渐显。对 box-shadow 添加过渡属于 defaults-fade-the-shadow。参见 recipes.md。
内容图片添加浅色轮廓
照片和屏幕截图应添加 1px 轮廓,向内缩进 1px;浅色模式下为 10% 黑色,深色模式下为 10% 白色。照片边缘与卡片融为一体,属于 defaults-image-outline。徽标和透明插图除外。参见 recipes.md。
卡片绝不嵌套
卡片绝不能置于另一张卡片内。将标题放在外部,让一个表面容纳内容。卡片内嵌卡片属于 defaults-no-nested-cards。
圆角,不用胶囊形
按钮和标签应使用圆角令牌。border-radius: 9999px 属于 defaults-rounded-not-pill。
不加装饰
移除装饰性渐变、光晕、模糊和深度效果。卡片后方的光晕属于 defaults-no-decoration。
主题应延伸至浏览器
深色主题应在根元素上设置 color-scheme: dark,让滚动条和原生控件与之保持一致。深色页面上出现浅色滚动条,属于 defaults-color-scheme。每种配色方案都应设置一个 theme-color 标签,使其与页面顶部颜色一致。
切换主题时,应关闭一帧的过渡效果。切换主题时所有颜色同时渐变,属于 defaults-quiet-theme-switch。参见 recipes.md。
仅为键盘操作显示焦点环
使用带有 outline 的 :focus-visible。每次点击都显示焦点环,属于 defaults-focus-keyboard-only。
对话框会控制焦点
对话框打开时会将焦点移入其中,按 Tab 键时焦点会留在对话框内,关闭时则将焦点还给触发它的元素。对话框打开时焦点仍留在后方页面上,属于 defaults-trap-and-return-focus。使用 showModal() 打开的原生 <dialog> 会处理焦点限制。
目标至少为 24px
每个交互目标较短的一边至少为 24px,并以鼠标操作时达到 40px、触屏操作时达到 44px 为目标。图标较小时,使用带负向内缩值的伪元素扩展点击区域。16px 图标按钮只有 16px 点击区域,属于 defaults-hit-area。扩展的点击区域绝不重叠。参见 recipes.md。
控件要说明自身用途
只有图标的按钮应带有说明其功能的 aria-label。没有名称的纯垃圾桶图标属于 defaults-name-icon-buttons。
状态不能只靠颜色表示。应搭配图标或文字。只用红点表示错误属于 defaults-not-color-alone。
链接就是链接
导航使用 <a href>,因此在新标签页中打开、中键点击和复制链接都能正常使用。通过 onClick 更改路由的 div 属于 defaults-links-are-links。
状态保存在 URL 中
筛选条件、标签页、分页和打开的面板都会写入 URL,因此刷新、分享和使用“后退”时都能保留状态。刷新后丢失的筛选条件属于 defaults-state-in-url。
缩放功能保持开启
视口标签中绝不设置 maximum-scale=1 或 user-scalable=no。阻止双指缩放的页面属于 defaults-allow-zoom。
点击体验符合原生习惯
在根元素上设置一次 -webkit-tap-highlight-color: transparent,并为每个可点击元素设置自己的 :active 状态。在触屏上点击后仍停留的灰色闪烁效果属于 defaults-no-tap-highlight。
按钮和链接设置 touch-action: manipulation,让点击无需等待第二次点击即可触发。延迟 300ms 才响应的点击属于 defaults-fast-tap。参见 recipes.md。
悬停样式需要悬停指针
将仅用于悬停的样式放在 @media (hover: hover) and (pointer: fine) 中,否则它们会在触屏上保持启用状态。点击后仍保持上浮状态的卡片属于 defaults-hover-capable-only。
全高布局使用动态单位
应用外壳或固定在底部的栏使用 100dvh,首屏使用 min-height: 100svh。底部按钮被 URL 栏遮挡的 100vh 外壳属于 defaults-dynamic-viewport。
内容避开刘海区域
在视口标签中添加 viewport-fit=cover,然后使用 env(safe-area-inset-*) 为固定页眉、标签栏、提示框和面板添加内边距。位于主屏幕指示条下方的底部栏属于 defaults-safe-areas。
控件不可选中
按钮、标签页、芯片和拖动手柄设置 user-select: none,这样长按时不会选中其标签。内容文本仍可选中。长按后高亮的按钮标签属于 defaults-controls-not-selectable。
快捷键不在输入框中触发
输入框、文本区域或可编辑元素获得焦点时,快捷键不会触发。这属于 defaults-keys-ignore-inputs。参见 recipes.md。
按 Enter 键提交
表单只有一个字段时,或焦点位于表单最后一个字段时,按 Enter 键即可提交。在文本区域中,按 Enter 键会换行,按 ⌘ 或 Ctrl 加 Enter 则会提交。需要点击才能提交的搜索框属于 defaults-enter-submits。
字段接受用户粘贴的内容
字段应接受用户实际输入或粘贴的内容,例如银行卡号中的空格或电话号码中的括号。在代码中去除格式,并清晰地显示值。拒绝 +1 (555) 010-2030 的电话号码字段属于 defaults-accept-messy-input。绝不阻止粘贴,并去除某些键盘自动添加的末尾空格。
字段选择正确的键盘
每个字段都设置 type、inputmode 和 autocomplete,以匹配相应的键盘和自动填充功能。将电子邮件字段设为纯文本类型属于 defaults-input-types。
在移动设备上,输入框应以 16px 或更大的字号显示,否则 iOS 会在输入框获得焦点时缩放页面。手机上使用 14px 输入框属于 defaults-input-16px。
标签共享点击区域
每个控件都有标签,点击标签即可操作控件。点击文本不起作用的复选框对应 defaults-label-hit-area。
表单在提交时验证
请求开始前,提交按钮始终保持启用。提交时,在每个字段旁显示对应错误,并聚焦第一个错误字段。表单有效前一直禁用的提交按钮对应 defaults-validate-on-submit。
忙碌状态的按钮保留标签
请求运行期间,按钮保留原有标签,显示指示器并禁用自身。用微调图标替换标签、导致按钮缩小的情况对应 defaults-loading-button。
危险操作可以撤销
危险操作后提供撤销选项。如果无法撤销,则在原处通过菜单或弹出框进行确认。既没有撤销选项也没有确认方式的删除操作对应 defaults-undo-destructive。
数字对齐
价格、计数、计时器和表格列使用 font-variant-numeric: tabular-nums。计数器随数值变化而宽度抖动的情况对应 defaults-tabular-numbers。
标题平衡排布,段落优雅换行
标题使用 text-wrap: balance,正文段落使用 text-wrap: pretty。标题最后一行只剩一个词的情况对应 defaults-balanced-wrap。
文本清晰呈现
在根元素上设置一次 -webkit-font-smoothing: antialiased 和 -moz-osx-font-smoothing: grayscale。缺少平滑设置,或在每个组件中分别设置,对应 defaults-antialiased-root。
行内代码字号更小
正文中的等宽字体缩小至 0.9em。行内代码仍保持完整字号、高出所在句子的情况对应 defaults-inline-mono。
截断文本可以查看完整内容
用户需要查看的截断文本,应能通过工具提示、展开控件或详情视图查看完整内容。被截断且无法查看完整内容的文件名对应 defaults-truncate-with-way-back。文件名和路径在中间截断,因为它们中间部分的差异更重要。数字、金额和日期绝不截断。
布局能够适应真实数据
使用最长的合理值测试每个字段。文本列设置 min-width: 0,头像等固定尺寸的容器设置 flex-shrink: 0,电子邮件地址和 URL 设置 overflow-wrap: anywhere。长电子邮件地址将行菜单挤出屏幕的情况对应 defaults-survives-long-content。
缺少可选字段时,应移除该行,而不是显示占位符。角色位置只显示一个短横线的情况对应 defaults-omit-empty-fields。
数字、日期和复数形式遵循区域设置
通过 Intl.NumberFormat、Intl.DateTimeFormat 和 Intl.PluralRules 格式化。显示 1 members 或原始值 1284 的情况对应 defaults-locale-format。
更新会主动播报
Toast、结果计数和行内验证信息应放在礼貌型实时区域 role="status" 中,该区域在文本变化前应为空。屏幕阅读器无法听到 Toast 的情况对应 defaults-announce-updates。
等待时立即显示反馈
操作应在 400ms 内响应,否则应立即显示反馈,例如乐观更新的值、骨架屏或进度条。加载分支返回 null 的情况对应 defaults-instant-feedback。
将微调图标或骨架屏延迟 150–300ms 显示,显示后至少保持 300–500ms。骨架屏闪现一帧的情况对应 defaults-no-flicker-loading。骨架屏应与其替代的布局相匹配。参见 recipes.md。
声音用于确认,绝不单独使用
声音适用于付款、上传或提交等确认场景,也适用于不容错过的错误提示。每个声音提示都应有对应的可见反馈,例如状态变化。成功提示音响起但屏幕上没有任何变化的情况对应 defaults-sound-has-a-twin。
输入、悬停、滚动和键盘导航不应发出声音。每次按键都发出点击声的情况对应 defaults-no-sound-on-repeat。
用户可以关闭声音,且音量初始值为 0.3。硬编码 audio.volume = 1 且没有关闭开关的情况对应 defaults-sound-optional。播放和合成点击声的示例见 recipes.md。
一次只打开一个对话框
绝不要在一个对话框上方再打开另一个对话框。改用菜单、弹出框或新页面。这对应 defaults-no-stacked-dialogs。
两者
每项检查都是一对。出错的行旁边就是替换它的行。
| 规则 | 错误做法 | 替代方案 |
|---|---|---|
defaults-fade-not-cut | 使用 overflow: auto,内容却被硬生生截断 | 两端使用 mask-image 渐隐 |
defaults-no-clipped-shadow | 卡片阴影被 overflow: hidden 截断 | 在滚动容器上设置 padding: 8px |
defaults-scroll-not-wrap | 代码行换成两行显示 | 使用 white-space: nowrap 和 overflow-x: auto |
defaults-contain-overscroll | 抽屉列表滚动到底部时,页面也跟着滚动 | 在抽屉上设置 overscroll-behavior: contain |
defaults-sticky-border | 粘性页眉没有边缘分隔线 | border-bottom: 1px solid var(--gray-a4) |
defaults-clear-sticky-chrome | 聚焦的字段被 56px 高的粘性页眉遮挡 | scroll-padding-block-start: 56px |
defaults-ring-not-border | 弹出层同时使用 border: 1px solid #e5e5e5 和 box-shadow | 先在阴影中使用 0 0 0 1px var(--gray-a4),边框设为透明 |
defaults-layered-shadows | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) | 使用三个向下的阴影层,逐层增大模糊、降低不透明度 |
defaults-fade-the-shadow | transition: box-shadow 200ms | 在承载凸起阴影的 ::after 上设置 opacity |
defaults-image-outline | 白色卡片上的照片没有边缘 | 设置 outline: 1px solid rgba(0, 0, 0, 0.1),并设 outline-offset: -1px |
defaults-no-nested-cards | 卡片内放着标题和另一张卡片 | 标题放在一张卡片上方 |
defaults-rounded-not-pill | 按钮使用 border-radius: 9999px | border-radius: var(--radius-2) |
defaults-no-decoration | 卡片后方有渐变光晕 | 使用纯色背景 |
defaults-color-scheme | 深色主题却使用浅色滚动条 | 在根元素上设置 color-scheme: dark |
defaults-quiet-theme-switch | 切换主题时所有颜色过渡同时触发 | 关闭过渡一个帧 |
defaults-focus-keyboard-only | .button:focus { box-shadow: 0 0 0 2px var(--accent) } | .button:focus-visible { outline: 2px solid var(--accent) } |
defaults-trap-and-return-focus | 按 Tab 键会聚焦到对话框后面的页面 | 将焦点限制在对话框内,然后返回触发元素 |
defaults-hit-area | 16px 图标按钮的点击区域也是 16px | 在其 ::before 上设置 inset: -12px,形成 40px 的点击区域 |
defaults-name-icon-buttons | <button><TrashIcon /></button> | <button aria-label="Delete"> |
defaults-not-color-alone | 仅用红点表示错误 | 红点搭配图标或单词 Failed |
defaults-links-are-links | <div onClick={() => router.push("/settings")}> | <a href="/settings"> |
defaults-state-in-url | 筛选条件仅存储在 useState 中 | 在 URL 中使用 ?status=open |
defaults-allow-zoom | maximum-scale=1, user-scalable=no | width=device-width, initial-scale=1 |
defaults-no-tap-highlight | 每次点按时浏览器都会闪现灰色高亮 | 设置 -webkit-tap-highlight-color: transparent,并添加 :active 样式 |
defaults-fast-tap | 按钮在点按 300ms 后才响应 | touch-action: manipulation |
defaults-hover-capable-only | .card:hover { transform: translateY(-2px) } | 将相同规则放在 @media (hover: hover) and (pointer: fine) 内 |
defaults-dynamic-viewport | 在应用外壳上设置 height: 100vh | height: 100dvh |
defaults-safe-areas | 标签栏被主屏幕指示条遮挡 | 配合 viewport-fit=cover 设置 padding-bottom: env(safe-area-inset-bottom) |
defaults-controls-not-selectable | 长按会选中标签的文字 | 在标签上设置 user-select: none |
defaults-keys-ignore-inputs | 在搜索框中输入时,按 J 会移动选中项 | 快捷键忽略 input、textarea 和 [contenteditable] |
defaults-enter-submits | 搜索字段外没有包裹 form | 使用 form,并在其 onSubmit 中执行搜索 |
defaults-accept-messy-input | 电话号码字段拒绝 +1 (555) 010-2030 | 接受输入,存储为 +15550102030,并以格式化后的形式显示 |
defaults-input-types | 邮箱输入使用 <input type="text"> | type="email" autocomplete="email" |
defaults-input-16px | 移动端输入框使用 font-size: 14px | font-size: 16px |
defaults-label-hit-area | 复选框旁边是独立的 span | 将复选框和文字一起放在一个 label 中 |
defaults-validate-on-submit | 提交按钮使用 disabled={!isValid} | 保持启用,提交时显示错误并聚焦第一个错误 |
defaults-loading-button | 将按钮标签替换为加载动画 | 保留标签,在旁边显示加载动画,并禁用按钮 |
defaults-undo-destructive | 删除操作立即执行,且无法撤销 | 显示带有 Undo 的提示条 |
defaults-tabular-numbers | 计时器数字变化时宽度也跟着变化 | font-variant-numeric: tabular-nums |
defaults-balanced-wrap | 标题最后一行只有一个单词 | h1 { text-wrap: balance } 和 p { text-wrap: pretty } |
defaults-antialiased-root | 浅色文字使用默认的子像素平滑 | 在根元素上设置 -webkit-font-smoothing: antialiased |
defaults-inline-mono | <code> 与所在句子的字号相同 | font-size: 0.9em |
defaults-truncate-with-way-back | 文件名被截断,且没有其他查看方式 | 使用工具提示显示完整名称 |
defaults-survives-long-content | 长邮箱地址把行菜单挤出屏幕 | 在文本上设置 min-width: 0,在菜单上设置 flex-shrink: 0 |
defaults-omit-empty-fields | 缺少角色时显示破折号 | 省略角色所在行 |
defaults-locale-format | {count} members 在 count 为 1 时仍显示复数 | 使用 Intl.PluralRules 选择 member 或 members |
defaults-announce-updates | 提示条挂载时带有文字,却没有 role | 创建一个空的 role="status" 区域,并在其中更新文字 |
defaults-instant-feedback | if (isLoading) return null | if (isLoading) return <Skeleton /> |
defaults-no-flicker-loading | 对于 40ms 就返回的请求也显示加载动画 | 200ms 后才显示,并至少保持 400ms |
defaults-sound-has-a-twin | 执行 playSound("success"),却没有状态变化 | 在播放声音的同时执行 setStatus("success") |
defaults-no-sound-on-repeat | 在 onChange 中调用 playSound("keystroke") | 输入时不播放声音 |
defaults-sound-optional | audio.volume = 1,且没有关闭开关 | 提供声音设置,默认音量为 0.3 |
defaults-no-stacked-dialogs | 从设置对话框中打开确认对话框 | 在对话框内使用菜单或弹出层 |
报告
严重程度。 HIGH 会影响使用或访问。例如,阴影被裁切、滚动被硬性截断、打字时触发快捷键、对话框层叠、目标尺寸小于 24px 或静默等待。还包括拒绝粘贴格式的字段、没有视觉对应项或关闭开关的声音。还包括没有名称的图标按钮、仅用颜色表示的状态、焦点逃出对话框、缩放被阻止或没有链接的路由。还包括必须先填写有效内容才能提交的表单、被截断且无法还原的值,或没有撤销或确认步骤的破坏性操作。还包括因值过长而被推到屏幕外的内容、隐藏控件的 100vh 外壳,或位于主页指示条下方的栏。
MEDIUM 包括胶囊按钮、点击时出现的焦点环,或在触屏上卡住的悬停效果。还包括数据中的比例数字、频繁操作时播放的声音、从覆盖层泄漏的滚动、刷新后丢失的状态,或闪烁的骨架屏。还包括使用错误键盘的字段、小于 16px 的移动端输入框、失效的标签、按 Enter 无法提交,或没有指示器的忙碌按钮。还包括未播报的更新、被固定界面元素遮挡的字段,或卡片中嵌套卡片。还包括点击闪光或点击延迟、可选择的控件标签,或未格式化的数字或复数形式。
LOW 包括阴影旁边不明显的边框、装饰性光晕、单个刺眼的阴影,或经过过渡动画的 box-shadow。还包括不平衡的标题、缺少轮廓的图像、没有边框的固定页眉、深色页面上的浅色滚动条、模糊的主题切换开关,或缺少根级字体平滑设置。还包括全尺寸的行内代码,或用于表示空字段的占位短横线。
格式。 每个根本原因列出一条发现:
file:line - [rule-id] what is wrong
然后提供 Rule | Count | Severity 汇总。仍有任何 HIGH 时使用 Block,否则使用 Approve。切勿对未阅读的文件使用 Approve。如果检查依赖布局,请打开屏幕;将所有无法运行的检查标记为 Not verified。