Hallmark 交互状态工程:八状态模型、无位移表单与对比度纪律的完整实现指南

Hallmark 交互状态工程:八状态模型、无位移表单与对比度纪律的完整实现指南 Hallmark 交互状态工程八状态模型、无位移表单与对比度纪律的完整实现指南【免费下载链接】hallmarkAnti-AI-slop design skill for Claude Code, Cursor, and Codex.项目地址: https://gitcode.com/GitHub_Trending/hal/hallmark本文是 Hallmarkskills/hallmark/SKILL.md 中定义的 Anti-AI-slop 设计技能对交互与状态interaction-and-states这一核心纪律的展开解读。文章覆盖从每个可交互元素的八种状态、:focus-visible焦点环、44px 命中目标到表单输入框的无位移状态配方、模态框/下拉/弹层的现代原生 API再到发货前必须通过的对比度五项闸门slop-test 46–50与--color-accent-inkToken 契约。读完你会得到一套可以直接复制进生产代码的 CSS 状态配方以及能用于审计任何现有 UI 的逐条检查清单。为什么两种状态的界面会断裂大多数 AI 生成的 UI 只做了两件事默认态和 hover 态然后就把剩下的全忘了。Hallmark 的判断非常直接每个可交互元素有八个状态缺任何一个这个元素就不算完成。缺状态不是没做细节而是界面在真实使用中会断裂的地方——键盘用户看不到焦点环、加载时用户被锁死在字段外、错误只靠一个红框传递、禁用元素毫无解释。这条纪律不是建议而是强制要求。在 SKILL.md 的 Component-scope 流程中写明State discipline — STRICTER. Every interactive component MUST ship code for all 8 states八状态清单是强制性mandatory而非可选的并明确指向本文作为唯一权威清单。八状态模型每一态在何时出现、该做什么状态何时出现处理方式Default 默认静止时基础样式Hover 悬停指针悬停仅在media (hover: hover)内小幅位移颜色变化、1px 平移、微妙边框Focus 聚焦键盘或编程式聚焦可见焦点环:focus-visibleActive / Pressed 按下按下过程中压入感变暗、translate(0 1px)Disabled 禁用不可交互降低透明度0.5cursor: not-allowedaria-disabledLoading 加载中处理中行内 spinner 或进度条标签保持可读Error 错误失败态红色边框、错误图标、错误消息、aria-invalidSuccess 成功已完成绿色对勾、确认提示、自动消失注意几个容易误解的细节hover 必须包在media (hover: hover)里否则触屏用户会被永久卡在悬停态上touch 设备上的 hover 一旦触发就不会消失loading 态是可编辑的加载不是把字段禁掉error 与 success 都要求颜色之外的第二信号图标 文字因为约 8% 的男性有色觉缺陷纯颜色状态在任何规范里都是失败。仓库中的真实实现印证了这一点。在 site/examples/bananastudio/styles.css 中按钮的四个指针状态被依次实现.btn:hover { transform: translateY(-2px); box-shadow: 0 14px 40px -8px var(--color-glow); background: var(--color-accent-2); } .btn:active { transform: translateY(0); transition-duration: 60ms; } .btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 3px; } .btn:disabled, .btn[aria-disabledtrue] { opacity: 0.55; cursor: not-allowed; pointer-events: none; }translateY(-2px)的悬停抬升、translateY(0)的按下回弹60ms 快速过渡模拟按压手感、禁用态的三重信号透明度 光标 aria-disabled——正是八状态模型的逐条落地。焦点环键盘可见性是硬性要求焦点环要求每个可交互元素上始终可见。浏览器默认焦点环是可接受的自定义焦点环更好:focus { outline: none; } :focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; border-radius: inherit; }四条硬性要求2–3px 宽与元素和页面背景的对比度均 ≥ 3:1距元素 2px 偏移用:focus-visible而非:focus保证只对键盘聚焦生效鼠标点击不出现焦点环永远不要无替代地outline: none。outline: none且没有其他焦点样式是最常见的无障碍 bug也是审计中立即判失败的一条。Hallmark 的全局基线在 site/css/base.css 中就是按此实现的先:focus { outline: none; }随后用:focus-visible绘制2px solid var(--color-focus)、偏移 3px、圆角 2px。整站所有交互控件都复用这个全局环例如 site/examples/hyperlane/styles.css 的.nav__cta:focus-visible以及 site/css/components.css 中复制按钮.code__copy、主题切换按钮、安装面板复制按钮等十余处:focus-visible声明。一个关键的配套约束来自 microinteractions.md焦点环的出现必须瞬时0ms禁止过渡opacity或transform。焦点环如果 200ms 渐入键盘用户会在过渡期间得不到任何位置指示——这是 AI 默认产物中最典型的 tell 之一。相关的反模式清单hover 上的 tooltip 延迟 800ms 但 focus 上必须 0ms、焦点环不随:focus而是:focus-visible出现等都记录在该文件中。命中目标触控可达的最小 44×44任何触控可达的元素命中目标最小44×44 CSS px。当视觉尺寸必须更小时用padding或::before覆盖层扩展命中区域而不改变视觉大小.icon-btn { position: relative; } .icon-btn::before { content: ; position: absolute; inset: -12px; }inset: -12px把可点击区域向外扩 12px视觉不变、触控命中变宽。同一原则适用于滑块的拇指thumbthumb 的视觉可以小于 44px但命中目标必须 ≥ 44px用透明扩展实现。表单标签、占位符、校验时机与错误关联标签放在输入框上方必须可见。绝不用 placeholder 当标签Placeholder 只表达格式不表达指令Placeholder: 01 Jan 2026而不是Placeholder: Enter your birth date辅助文本helper text在输入框下方错误文本替换辅助文本二者同位置同字号在 blur 时校验而不是每敲一个键就校验字段一旦被 blur 过touched模式之后每次变更都重新校验错误消息三要素(1) 哪里坏了 (2) 为什么 (3) 该怎么办一句话能说完就一句话用aria-describedby关联错误字段上设置aria-invalidtrue必填字段用aria-required标记绝不仅仅靠颜色提交按钮只在表单已知无效或提交中时禁用闲置态永远不禁用。site/examples/hyperlane/index.html 提供了一个贴近真实项目的样例邮箱输入框把标签放在span classrsvp__field-label中placeholderyoustudio.work只给出格式示例aria-describedbyrsvp-help关联下方辅助说明段落并且表单末尾还有output forrsvp-email aria-livepolite用于异步成功后静默播报——这正是错误关联 成功后轻柔确认的组合。输入框状态最容易差一点就对了的地方一个只有默认 hover两种状态、且 focus 时改了边框粗细的输入框会读起来像默认设置页——几何形状一移位眼睛立刻察觉整页就显得没调校好。每个文本框、文本域、select 和 combobox 都必须满足下面的每一条规则。无布局位移规则边框粗细在所有状态下恒定。默认 · hover · focus · error · disabled——border-width的值永远不变。focus 时的布局位移是明显的AI 生成痕迹。状态变化只能走background-color、outline或box-shadow绝不走border-width.input { border: 1px solid var(--color-rule-2); /* 1px, always — every state */ outline: 2px solid transparent; /* reserved slot for focus ring; no shift on activate */ outline-offset: 1px; }关键技巧outline 一开始就以 2px 透明占位焦点环出现时盒子几何已经正确——无布局位移无 paint thrash。逐状态配方状态处理方式原因Default 默认border: 1px solid var(--color-rule-2)·background: var(--color-paper)· placeholder 用var(--color-muted)可见的字段可读的空值信号Hover 悬停background: var(--color-paper-2)比 paper 深 4–6%· 边框不变细微的背景变化无边框闪烁。单独变边框颜色容易被忽略Focus 聚焦outline: 2px solid var(--color-focus)·outline-offset: 1px· 边框可加深到var(--color-ink-2)但宽度保持 1pxOutline 是焦点信号绝不动画化与页面和字段对比度均 ≥ 3:1Active / 输入中同 focus。不添加单独的输入中状态Focus 已经说了这里活跃第二个信号是噪音Filled 已填写同默认——由值本身携带状态。可选地加一条 subtle 的 ink-2 边框与空值区分不要用花哨的 chrome 变化与用户输入的内容争夺注意力Disabled 禁用opacity: 0.55·cursor: not-allowed· placeholder 用var(--color-rule-2)·aria-disabledtrue·tabindex-1三种独立信号透明度 光标 颜色不让单一通道承担全部负荷Error 错误border-color: var(--color-error, oklch(58% 0.20 25))· 辅助文本替换为错误消息 ·aria-invalidtrue· 右缘小 ⚠ 图标边框变色是 OK 的因为辅助文本和 aria 也在同时发出信号。绝不只靠颜色Success 成功subtle 强调色边框比默认高 3% 色度· 小 ✓ 图标 · 用户重新编辑时自动清除安静的反馈成功不值得庆祝除非它来之不易Loading 加载校验/异步中右缘行内 spinner 替换标准图标槽 · 字段保持可编辑但提交被禁用不要锁死用户——他们可能想改掉刚输入的内容高度与节奏输入框高度 按钮高度。44px 的按钮配 38px 的输入框整页就没调校好。选定一个基础高度44px 是触控下限应用到所有文本输入框和所有相邻按钮垂直 padding (height − line-height-px) / 2不要魔法数字预留右缘槽。每个输入框预留约 24px 的右缘空间给可选清除按钮、错误图标或加载 spinner不用时槽位留空——图标出现时绝不回流重排。标签、辅助文本、错误标签在输入框上方间隙 4–8px。绝不内联placeholder 当标签是硬伤辅助文本在下方间隙约 4px。与标签同字号、更低视觉权重错误替换辅助文本——同位置、同尺寸、错误色。两者绝不同时出现会导致校验时垂直跳动辅助文本保持稳定高度。即使为空也预留 1 行高度这样加错误时不会把页面顶下去在辅助容器上用min-height: 1lh。Dont 清单任何状态下都不要过渡border-width、padding或height——必然造成布局位移不要过渡焦点环的opacity或transform——焦点必须瞬时hover 效果要放进media (hover: hover)让触屏用户不陷入卡死状态不要把禁用字段当作等待中——用加载态字段保持可编辑不要在:focus上改cursor——指针已经是竖线beam了别跟它打架不要在 focus 上无替代地outline: none。具体控件的覆盖规则Textarea与输入框同规则另加resize: vertical小型 textarea 上绝不none也绝不both、min-height: 6rem保证多行体验Select只有能复刻原生无障碍键盘、读屏器时才做自定义select否则保留原生、只美化外层 wrapperCheckbox / radio现代浏览器上用accent-color: var(--color-accent)即可获得便宜又正确的样式只有设计上确有必要才自建。若自建仍要 1px offset 的焦点环Toggle / switch它就是 checkbox同样的无障碍契约视觉设计不改变契约Range / slider焦点态给 thumb 而不是 trackthumb 命中目标 ≥ 44px视觉可更小用透明扩展File input总是包在 styled label 里。原生input typefile不可样式化label 才是交互表面Combobox / searchlistbox 在下方aria-expanded镜像可见性方向键循环、Enter 选择、Escape 关闭——且 listbox 不推挤页面内容用position: absolute 父级position: relative。模态框与覆盖层用原生dialog元素——焦点陷阱、Escape 关闭、::backdrop样式全免费模态背后的页面内容设置inert防止 tab 顺序泄漏关闭途径Escape 键、点击背景、显式关闭按钮首个焦点落在第一个可交互元素上而不是关闭按钮。下拉、tooltip、popover用Popover APIpopover属性——light-dismiss、层级stacking、Escape 关闭全免费所有现代浏览器可用可用时用CSS Anchor Positioning定位降级为position: fixedgetBoundingClientRect()绝不把下拉放进overflow: hidden容器而不留出口——会被裁剪掉靠近视口边缘时翻转。用 Undo 代替 Confirm对于可逆操作跳过确认对话框。直接执行弹一个带 Undo 按钮的 toast5–10 秒对于破坏性、不可逆的操作删除账号、drop 表保留确认——而且要让用户输入被删除对象的名字而不是点一个OK。这一原则在 microinteractions.md 中有一套完整配方乐观更新 失败回滚 带 Undo 的 toast。成功时不弹 toastsilent success——如果用户能看见结果就不需要确认失败时总是弹错误 toast 并提供重试/撤销。加载态与空态内容形状可预测列表、卡片、表格时用skeleton 骨架屏而不是 spinner按钮内部状态用行内 spinner替换标签而不是加在标签旁边空态必须有小图标或插图、一行为什么是空的的说明、一个修复动作绝不显示毫无上下文的 No results。禁令清单Bansplaceholder 当标签仅 hover 的功能触屏用户无法 hover无替代地移除焦点环低风险动作的确认对话框命中目标 44px可交互元素上的自定义光标禁用元素却不解释为什么禁用纯颜色的错误态本应用骨架屏展示布局的地方却用了 spinner。对比度纪律slop-test 闸门 46–50Hallmark 的输出在发布前必须通过 slop-test 的46–50 五项对比度闸门见 slop-test.md。要逐对计算页面上每个(color, background-color)组合。Hallmark 输出最容易踩的四个坑翻转表面上的文字。.section--ink { background: var(--color-ink); }把表面翻成深色嵌套文字仍继承color: var(--color-ink)→ ink-on-ink。修复任何设置深色background的规则必须在同一条规则内同时设置color: var(--color-paper)强调色填充上的按钮文字。background: var(--color-accent); color: white;——但白色对 accent 只有 4.5:1 的对比度仅当--color-accent足够深时才成立。应改用var(--color-accent-ink)主题保证它通过 ≥ APCA Lc 60着色纸张上的弱化文字。color: var(--color-muted); background: var(--color-paper-3);——两者都是中等亮度经常跌破 4.5:1。改用--color-neutral更深或把背景提到--color-paper强调色按钮上的焦点环。按钮填充是--color-accent焦点环又是outline: 2px solid var(--color-focus)——如果--color-focus恰好等于--color-accent焦点环就消失了。用对比度配对--color-focus必须与元素和页面都 ≥ 3:1。计算方法对页面实际渲染的每个(文字色, 背景色)对运行APCA Lc首选感知均匀或WCAG 2.1 ratio预检查两者都在 OKLCH 且|L_a − L_b| 50%标记为需要完整计算正文文字通过线APCA Lc ≥ 60≈ WCAG 4.5:1大字 / 焦点环 / 图标通过线APCA Lc ≥ 45≈ WCAG 3:1。在 site/css/tokens.css 中12 个主题的每个--color-accent、--color-accent-ink、--color-focus都是按这一纪律独立挑选的。例如 Specimen 主题--color-focus: #FC4C02与 accent 同色因为按钮不常以 accent 填充而像 sections.css 里的 radio-tab 焦点环、components.css 里的复制按钮焦点环全部通过var(--color-focus)引用 token 而非硬编码颜色。Token 契约--color-accent-ink每个主题必须定义--color-accent-ink——即每当--color-accent填充一个承载文字的表面时要使用的文字颜色。accent-ink 在主题构建时就验证过与 accent 的对比度 ≥ APCA Lc 60。Hallmark 代码里只要用了background: var(--color-accent)就必须同时设置color: var(--color-accent-ink)。回退到硬编码color: white是硬伤——主题的 accent 可能是浅色白字压浅色就是 bug。tokens.css 展示了这条契约在不同色域下的具体取值深色主题如 Midnight、Aurora、Halo的 accent 是亮色accent-ink 因此取深色或同色系如 Aurora 的--color-accent-ink: oklch(11% 0.025 200)几乎就是纸色反转浅色主题如 Specimen、Brutal的 accent 是深色accent-ink 取亮色。这正是绝不硬编码 white的工程化产物。当表面被翻转规则任何覆盖background-color的规则必须同时声明合适的color。不要依赖继承来处理翻转表面的类/* WRONG — text inherits color: var(--color-ink); section is now dark; ink-on-ink */ .section--manifesto { background: var(--color-ink); } /* RIGHT */ .section--manifesto { background: var(--color-ink); color: var(--color-paper); }同理适用于按主题的覆盖如[data-thememanifesto] .vs__col:first-child { background: var(--color-ink); }——要么同时设置color: var(--color-paper)要么把规则声明在父元素上让后代显式继承。落地方式8 状态演示 wrapper 与状态印章为了让八个状态都真实渲染这件事可验证SKILL.md 要求组件产出的同时生成一个ComponentName.preview.html演示文件把组件以全部 8 个状态垂直堆叠、逐个标注。每个标注行用一个类如.is-hover配合真实的伪类选择器让 8 个状态在同一页面上同时可见.btn:hover, .btn.is-hover { background: var(--color-paper-3); } .btn:focus-visible, .btn.is-focus { outline: 2px solid var(--color-focus); } .btn:active, .btn.is-active { transform: translateY(1px); }组件输出还要带上状态印章其中states:一行就是逐状态的完成度清单/* Hallmark · component: type · genre: genre · theme: theme * states: default · hover · focus · active · disabled · loading · error · success * contrast: pass (46–50) */从这份印章回看整篇文章可以提炼出一个自检节奏先数八状态有没有缺再验焦点环与 44px 命中再查表单五条标签、placeholder、校验时机、错误关联、禁用时机最后过对比度闸门 46–50。按这个顺序逐条过完一个交互元素才能算完成——这正是 Hallmark 用它对抗 AI 默认产物的全部方法。【免费下载链接】hallmarkAnti-AI-slop design skill for Claude Code, Cursor, and Codex.项目地址: https://gitcode.com/GitHub_Trending/hal/hallmark创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考