Hallmark 滚动浮动导航全解:N10 Floating-on-Scroll Morph 的「同 DOM 双形态」交叉淡变实现
【免费下载链接】hallmarkAnti-AI-slop design skill for Claude Code, Cursor, and Codex.项目地址: https://gitcode.com/GitHub_Trending/hal/hallmark
导读
本文完整讲解 Hallmark(Anti-AI-slop 设计技能,用于 Claude Code、Cursor、Codex)中要求最高的导航组件 N10Floating-on-scroll morph:页面顶部的常规栏在滚动超过阈值后,以一次精心编排的动画形态变化为悬浮药丸(floating pill)。你将掌握其「四条不可协商法则」、12 个属性的交叉淡变映射表、经 rAF 节流与passive监听的滚动处理器,以及 Hallmark 明确拒绝的 8 种反模式,并对照仓库中 BananaStudio 示例的真实实现逐行验证。读完即可在任意项目中复刻这一「克制感」导航。
一、N10 是什么:从组件手册看它在导航谱系中的位置
在 Hallmark 的 组件烹饪书索引 中,导航共 10 个原型(N1–N10)。N10 的完整定义见 组件原型文件:
顶部粘性栏在用户滚动超过阈值后变形为悬浮药丸。两种视觉模式共享同一 DOM ——
.nav(外层)持有栏的视觉,.nav__inner(内层)持有药丸的视觉。通过单一类切换(.is-floating)与一条时间曲线交叉淡变。
两条使用要点:
- Use when:atmospheric / modern-minimal 风格的页面,这类「微妙的动力学时刻」值得出现时使用。它只添加一个恰到好处的惊喜,并抵抗新奇性诱惑。
- Don't confuse with:N5 Floating pill(始终悬浮,无滚动行为)。N10 是 N5 加上一个默认栏状态、并在滚动时变形进入N5 的形态。
组件手册的导航路由表中,modern-minimal 与 atmospheric 的默认导航是 N5;N10 适合作为「既有默认栏、又想给滚动加上一记克制的动态」的进阶选择。文档原话是:「AI 的默认输出会把下面四条法则全部搞砸——这正是 N10 成为整个烹饪书中要求最高的导航的原因。」
二、结构:一个<header>,两层视觉,绝无第二个 DOM
N10 的骨架只有一层<header>,内部一个包裹层。外层拥有默认栏的视觉,内层拥有悬浮药丸的视觉:
<header class="nav"> <div class="nav__inner"> <a class="wordmark">Hallmark</a> <ul class="nav__links">…</ul> <a class="cta">Install</a> </div> </header>当.is-floating类随滚动阈值切换时,每一层交叉淡出自己的视觉、同时淡入另一层的视觉。整个状态翻转只通过一个类、一条时间曲线完成。
对照仓库中的真实实现 BananaStudio 示例,HTML 结构完全一致,且 CSS 在 styles.css 中为两层分别声明了默认与浮动状态。为什么必须是单 DOM?看反模式第 1 条便知:双<header>透明度互换会翻倍标记、打乱焦点顺序(focus order)、还可能让两层内容失同步。
三、四条不可协商法则(Four Laws)
法则 1:导航总高度必须恒定
如果状态翻转时外层高度发生变化,其下方的每一个像素都会垂直跳动——用户感知为「滚动中页面跳了一下」。解决方法是:用外层的padding-block补偿内层的收缩,使两种状态下数学总和相同。
以 BananaStudio 为例(border-box 语义下):
| 状态 | 外层.navpadding-block | 内层.nav__innermin-height | 合计 |
|---|---|---|---|
| 默认 | 0 | 60px | 60px |
| 浮动 | var(--space-2xs)=0.25rem= 4px ×2 | 52px | 52px + 8px = 60px |
两种状态都收敛到 60px——这就是「高度恒定法则」的数值验证。仓库注释也明说:「padding-blockkeeps total nav height at 60px (no jitter)」。
法则 2:可见偏移用transform: translateY(),绝不用padding/margin
悬浮药丸应与视口顶部保持呼吸距离。天真的「给外层加 padding」修复会破坏法则 1。transform不影响布局——药丸在视觉上下沉,盒模型纹丝不动,页面内容不会随之跳动。
BananaStudio 实现中,药丸下沉完全由transform: translateY(12px)承担(注释:「pure transform, so it doesn't change layout / shift content below」),药丸距顶约 16px,而下方透明的.nav继续铺满宽度。
法则 3:每一处共享视觉都必须明确归属交叉淡变
对于外层在默认态持有的每个属性(背景、边框、backdrop-filter、box-shadow),在.is-floating状态下显式中和它,并把它写进过渡列表。漏掉任何一个——比如浮动态下外层残留一个backdrop-filter: blur(14px)——就会出现一条看不见的模糊条带拖过视口、背后空无一物。
看 BananaStudio 的实现:外层.nav默认有background、border-block-end、backdrop-filter: saturate(1.4) blur(14px)三项;浮动态下每一项都被显式中和为transparent/transparent/blur(0),且全部在transition列表中逐一声明。
法则 4:所有属性共用一条时间曲线
八个属性配八条曲线,读起来就是八个动画;同样的八个属性配上var(--dur-mid)+var(--ease-out),读起来就是一个动作。使用cubic-bezier(0.16, 1, 0.3, 1)(指数型缓出)与约 520ms。
仓库 token 定义完全印证这两点:tokens.css 中--ease-out: cubic-bezier(0.16, 1, 0.3, 1);BananaStudio 的 tokens.css 中--dur-mid: 520ms。
四、属性变形映射表:12 个属性、一条曲线
以下是文档给出的完整属性映射表,两个状态间每个属性都参与同一条曲线的过渡:
| 元素 | 属性 | 默认 | 浮动 |
|---|---|---|---|
.nav | padding-block | 0 | var(--space-2xs) |
.nav | background-color | dark/0.62 | transparent |
.nav | border-block-end-color | rule | transparent |
.nav | backdrop-filter | saturate(1.4) blur(14px) | blur(0) |
.nav__inner | max-width | var(--page-max) | ~58rem |
.nav__inner | min-height | 60px | 52px |
.nav__inner | padding-block | 12px | 4px |
.nav__inner | border-radius | 0 | var(--radius-pill, 999px) |
.nav__inner | background-color | transparent | dark/0.82 |
.nav__inner | backdrop-filter | blur(0) | blur(18px) |
.nav__inner | box-shadow | none | drop + tinted glow + inset hairline |
.nav__inner | transform | translateY(0) | translateY(12px) |
关键技术点:用blur(0)而不是none——none是瞬间跳变,blur(0)才能参与过渡。backdrop-filter的过渡是 2024+ 的浏览器基线能力(Chrome 107+、Safari 14+、Firefox 103+),使用前需确认目标浏览器范围。
BananaStudio 实现的浮动态药丸阴影栈,正好对应表中「drop + tinted glow + inset hairline」:
box-shadow: 0 18px 50px -18px oklch(0% 0 0 / 0.6), /* drop 投影 */ 0 4px 14px -4px oklch(0% 0 0 / 0.35), /* 近距投影 */ inset 0 0 0 1px oklch(98% 0.005 80 / 0.05), /* inset 发丝线 */ 0 0 36px -8px var(--color-glow-soft); /* 主题色调微光 */而内层默认态同样遵守「显式中和」:box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent(而不是none)、border: 1px solid transparent、background: ... / 0、blur(0)、translateY(0)——每一个占位属性都为过渡准备好了可插值的起点。
五、滚动处理器:passive + rAF 节流 + 布尔翻转守卫
文档给出的完整脚本(BananaStudio 中的 内联实现 与之逐行对应):
(() => { const nav = document.querySelector(".nav"); if (!nav) return; const THRESHOLD = 80; // ≥ 60 px to avoid micro-scroll twitches let floating = false; let ticking = false; const update = () => { const next = window.scrollY > THRESHOLD; if (next !== floating) { // boolean-flip guard — toggle once per state change floating = next; nav.classList.toggle("is-floating", floating); } }; window.addEventListener("scroll", () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { update(); ticking = false; }); }, { passive: true }); // mobile scroll-perf — keep main thread free update(); })();三个纪律要点,缺一不可:
passive: true—— 监听器不会调用preventDefault,浏览器可以保持主线程空闲来滚动,是移动端滚动性能的关键。若省略,浏览器必须假设每个滚动事件都可能被取消,从而无法启用滚动优化。requestAnimationFrame节流—— 将回调限制在每秒最多约 60 次,并与绘制帧对齐,避免滚动事件高频触发布局抖动。- 布尔翻转守卫——
if (next !== floating)保证类操作只在状态真正变化时执行一次,而不是每个滚动事件都执行一次。文档明确警告:在每次滚动事件上反复toggle类会导致 layout thrash(布局震荡)。
此外脚本开头调用一次update(),保证页面加载在阈值以下时初始状态正确(不悬浮)。
六、Hallmark 拒绝的 8 种反模式
文档明确列出 8 条,全部建议当作实现时的「禁止清单」:
- 两个独立的
<header>元素通过透明度互换—— 翻倍 DOM、与焦点顺序作对、可能让两层内容失同步。N10 的正解永远是单 DOM 双形态。 - 用
top/margin-top制造浮动偏移—— 触发布局计算。transform是唯一正确的杠杆(对应法则 2)。 - 浮动态使用
backdrop-filter: none—— 瞬间跳变。用blur(0)让它平滑过渡(对应属性表的注记)。 - 导航高度在两种状态间变化—— 引起内容抖动,是这个模式里破坏力最大的一处错误(对应法则 1)。
- 每个属性用不同的过渡时长(「加快圆角」「延迟阴影」)—— 虽然每个属性都在动,但整体读起来是坏的。一条曲线(对应法则 4)。
scroll事件不带{ passive: true }或不做 rAF 节流—— 二者都会拖垮移动端滚动性能。- 阈值 = 0(轻轻一滚就触发变形)—— 太跳跃。应使用 ≥ 60px 的阈值。
- 漏掉布尔翻转守卫—— 在每个滚动事件上切换类导致布局震荡。
七、落地指南:token 化与移动端
要在项目中复用这个模式,遵循 Hallmark 的 token 纪律:所有取值引用命名 token,不内联魔法值。可直接参照 tokens.css 的约定:
- 时长/缓动:
--dur-mid: 520ms(BananaStudio tokens)+--ease-out: cubic-bezier(0.16, 1, 0.3, 1)(site tokens)。 - 间距:
--space-2xs(0.25rem)、--space-sm(0.75rem)、--space-md(1rem)等 4pt 语义间距。 - 圆角:
var(--radius-pill, 999px)——注意 soft 系主题(Quiet、Bloom、Coral、Violet、Aurora、Plume)在 tokens.css 主题覆写区 中把--radius-pill定义为999px;editorial 主题则为0,此时药丸形态会退化为直角胶囊,需按主题裁量。 - 层级:
.nav使用position: sticky; top: 0,z-index 可参考--z-sticky-nav: 300(见 tokens.css),确保导航始终压过页内其他 sticky 元素。
移动端可参考组件手册的横切规则:低于 40rem 时禁用滚动联动动画(移动端滚动自带物理特性,叠加动画会互相打架);触控目标保持 ≥ 44×44px;低于 60rem 时若药丸容纳不下完整链接列表,可保留 wordmark + CTA 并保持悬浮形态(对应 N5 的折叠行为)。
总结
N10 之所以是烹饪书中「要求最高的导航」,是因为它把约束本身当作设计:一个 DOM、两种视觉模式、一次类切换、一条时间曲线。它之所以能成立,恰恰因为克制——一次精心编排的变形,没有任何多余的装饰。把精力投入三件事:时间曲线(cubic-bezier(0.16, 1, 0.3, 1)+520ms)、阴影栈(drop + tinted glow + inset hairline)、以及高度恒定数学(外层padding-block补偿内层收缩),而不是投入「更多效果」。对照 floating-nav.md 原文档与 BananaStudio 实现,你就可以在 atmospheric / modern-minimal 页面中,亲手复刻这个 AI 默认输出永远做不好的克制时刻。
【免费下载链接】hallmarkAnti-AI-slop design skill for Claude Code, Cursor, and Codex.项目地址: https://gitcode.com/GitHub_Trending/hal/hallmark
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考