关于 glimm
glimm 是为 Web 项目添加 WebGL 色带页面转场的技能,提供跨框架接入指导,并强调适用场景评估、无障碍行为和版本适配。
主要功能
- 01跨框架转场核心
- 02React 和 Next.js 适配器
- 03自动拦截站内链接
- 04应用内状态切换转场
- 05减少动态效果及 WebGL 回退
适用场景
- 01为作品集添加品牌转场
- 02在应用内操作后显示结果
- 03评估频繁导航是否适合转场
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nomanjack/ glimm-skill --skill glimm安装后在 Claude Code 中输入/glimm确认已加载。
附带文件references/react-and-vanilla.mdreferences/tuning.md
详细介绍
glimm — 色彩扫掠页面过渡
glimm 会在导航期间让一束 WebGL 色彩扫过视口:离开的页面始终位于色带前方,新页面则在色带后方显现(遮罩式页面过渡)。核心部分与框架无关,并提供 React 和 Next.js 适配器。零运行时依赖,MIT。
此技能适用于任何 glimm 版本。编写代码前,先检查项目解析到的版本(npm ls glimm,或已安装的 node_modules/glimm/package.json),并以已安装的包为准:确认你使用的每个导出项都存在于其类型定义中(node_modules/glimm/dist/*.d.ts)。如果下面提到的 API 在其中缺失或名称不同,请遵循已安装版本及其更新日志(https://glimm.dev/changelog),不要照搬这里的方案。全新安装时,优先使用 npm install glimm@latest。
页面朝交互位置靠拢、页面随色带弯曲,以及跟随正在进行的请求的时序,都由 GlimmScope 提供,该功能在 glimm 0.4 中加入。如果已安装的类型定义中没有它,请告诉用户,运行 npm install glimm@latest 即可获得。不要用自定义着色器重新实现这些效果。
范围:只实现 glimm,不做其他任何事
只添加并接入 glimm。项目的其他一切都由项目所有者负责,包括文案、错误消息、加载设计、布局、样式和应用逻辑。除非用户明确要求,否则不要编写或重写其中任何内容,也不要围绕扫掠效果添加功能。如果附近有什么看起来不对,用一句话指出,然后保持原样。
第 1 步 — 适用性检查(修改代码前)
glimm 是一种鲜明的表达,不是默认选择。每次导航时,扫掠效果都会占用用户大约一秒的注意力,因此适合每次导航都代表有意义场景转换的网站;对于导航频率很高的工具,它则会很快令人厌烦。先查看项目(框架、路由器、网站类型),并给出诚实的判断:
适合: 营销网站、作品集、个人网站、博客和长篇阅读、产品发布页面、按章节阅读的文档、画廊——任何访客每次会话只导航几次,且品牌个性很重要的场景。
不适合: 管理仪表盘、数据表、设置面板、多步骤表单流程中途、搜索与筛选界面,以及用户每小时会导航数十次的任何场景。每次点击侧边栏都进行 1.1 秒的扫掠,到第三次点击就会变成阻碍。
如果项目不适合,请直言并建议不要安装——正是这份诚实,才能让“适合”的判断值得信赖。如果用户无论如何都想使用,请尊重其选择,但限定使用范围:缩短时长(sweepMs: 650、outroMs: 150),并且只在主要区块切换时执行扫掠;对于高频链接,则通过 data-glimm-skip 排除(第 4 步)。
这个判断针对的是每次导航都执行扫掠的情况。即使某个产品不适合对每个链接都使用扫掠,只要它发生得足够少,且改变的是用户在意的内容,仍然可以在某个特定时刻使用一次。这种情况下,跳过 InterceptLinks,改用第 2 步中的应用内方案。
无障碍和可靠性都不是需要犹豫的理由:glimm 会尊重 prefers-reduced-motion(立即导航),并在 WebGL 不可用时降级为普通导航。这两项都是自动处理的——不要自行编写减少动态效果的保护逻辑。
第 2 步 — 安装并接入
npm install glimm
react 和 next 是可选的对等依赖——不会引入其他依赖。
Next.js App Router(常见情况)
在根布局中挂载一次,并包裹正文内容:
// app/layout.tsx
import { GlimmProvider, InterceptLinks } from 'glimm/next'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GlimmProvider palette="prism">
<InterceptLinks />
{children}
</GlimmProvider>
</body>
</html>
)
}
安装就这些。<InterceptLinks /> 会自动为每次内部链接点击执行过渡扫描——普通的 <a> 和 next/link 都无需改动即可使用,并且它已自动忽略:修饰键点击、中键点击、target="_blank"、下载链接、外部来源和同页哈希链接。不要重新实现这些保护逻辑。
重要的接入规则及其原因:
- 使用根布局,而非嵌套布局。 用户跨布局边界导航时,嵌套布局中的提供器会重新挂载,中断着色器上下文并丢弃过渡效果。
- 每个应用只有一个入口点。 从
glimm/next(或在非 Next 应用中,从glimm/react)导入所有内容。混用入口点会创建两个 React 上下文,一个入口点的useGlimm()无法获取另一个入口点提供的上下文。 - 根布局可以继续作为服务端组件。
GlimmProvider和InterceptLinks已经是客户端组件;在布局文件中导入它们不需要添加'use client'。
控制单次导航(可选,仅在需要的地方添加)
'use client'
import { TransitionLink, useTransitionRouter } from 'glimm/next'
// Declarative — a drop-in next/link replacement:
<TransitionLink href="/work" sweep={{ palette: 'berry', direction: 'rtl' }}>
Work
</TransitionLink>
// Programmatic — for form submits / post-action redirects:
const router = useTransitionRouter()
await save(form)
router.push('/done', { palette: 'ember' })
TransitionLink 和 InterceptLinks 可以安全共存——链接自身的处理器会接管点击,拦截器会发现该点击已处理,因此不会重复触发。
应用内时刻
过渡扫描不需要路由变更。它可以标记当前页面上的状态变化,而这往往是在产品内部使用它的更好方式。有两种实现方法;只要所安装的版本支持,GlimmScope 就是更好的选择:
GlimmScope包裹一个区域,可以是页面、版块或卡片。它会执行应用的实际工作,将变化隐藏在光带之后,并在浏览器支持的情况下将内容移到光带下方。请参阅下一节。useGlimm中的sweep()会覆盖视口,无需更改页面结构。没有GlimmScope的版本,或用户只想显示光带时,可选择这种方式。
'use client'
import { useGlimm } from 'glimm/next' // or 'glimm/react' outside Next
const { sweep } = useGlimm()
sweep(() => setPlan('pro'), { sweepMs: 800 })
只有当某个时刻足够少见,且用户会希望注意到它时,才值得触发过渡扫描:升级套餐、完成付款、启用某种模式、获得等待已久的结果、队友加入。常规操作不值得触发,包括保存、编辑行、勾选复选框,以及会话中反复执行多次的任何操作。没有人主动触发的事件(例如收到付款)也可以用同样的方式触发扫描,只需在处理该事件的位置调用即可。sweep() 的持续时间应短于导航过渡,约为 650 到 900 ms。
使用 GlimmScope 移动页面
'use client'
import { useRef, useState } from 'react'
import { GlimmScope, type GlimmScopeHandle } from 'glimm/next' // or 'glimm/react'
const scope = useRef<GlimmScopeHandle>(null)
const upgrade = (event: React.MouseEvent) => {
const r = event.currentTarget.getBoundingClientRect()
setBusy(true) // answers the press, outside the scope's timing
scope.current?.play({
at: [r.left + r.width / 2, r.top + r.height / 2], // the page leans in towards the button
work: async () => {
const res = await fetch('/api/upgrade', { method: 'POST' })
if (!res.ok) throw new Error('Upgrade failed') // a rejection means no sweep
},
change: () => setPlan('pro'), // runs as soon as the work is done, behind the band
onBurst: () => setBusy(false), // the band has broken away
onFail: () => setBusy(false), // no sweep, and the page stays as it was
})
}
<GlimmScope ref={scope} curve={0.12} direction="ttb" sweepMs={2700} style={{ position: 'absolute', inset: 0 }}>
<Billing />
</GlimmScope>
它不需要 GlimmProvider,因此只想使用某个效果的项目可以单独使用它。使用规则如下:
- 不要检查浏览器是否支持。 在支持 HTML-in-Canvas 的地方,内容会向动作方向靠拢,并在光带经过时泛起涟漪。其他地方则会在同一个框内呈现普通扫过效果,变化仍然在光带之后发生。它也会处理减少动态效果的设置,因此绝不要编写保护条件或第二条代码路径,也不要因为浏览器版本而放弃某项功能。这就是它可以放心使用、无需询问的原因。
work执行实际工作,change接收结果。 两者都放在play()中,因此不要像使用sweep()那样先等待请求完成再调用。按下时工作即开始,页面会在运行期间向内靠拢,无论耗时多久,整个移动过程都如同一口气完成。change会接收工作结果。失败时要让work拒绝,包括响应状态错误的情况,因为fetch遇到 500 时不会抛出异常。- 按下控件的忙碌状态由外部管理。 在调用
play()前设置,在onBurst和onFail中清除,不要等待onLeave。 - 仅在无人按下任何控件的情况下传入
anticipate: true,例如收到付款时。它会让页面有时间充分向内靠拢后再发生变化;如果有人刚请求了某项操作,这就意味着要让其等待。不传入work时,change会在按下时立即运行。 - 后续的
play()会中断当前播放, 因此不要将播放排队,也不要在一次播放结束前禁用控件。play()还会返回{ fastForward, cancel }。 - 为框指定尺寸。 作用域会裁剪到自身的框并填满该框,因此要用
style设置尺寸,并适配现有布局。示例中的position: 'absolute', inset: 0适用于定位容器内的视图;如果没有定位祖先元素,它会填满视口,因此不要在正常文档流中的页面上使用。在 flex 容器中,flex: 1, minHeight: 0会填满内容区域。区块或卡片需要设置固定高度,因为页面效果运行时,内容不会为框提供尺寸,框会因此收缩为零。可滚动内容必须在作用域内滚动,而不是在作用域外的文档中滚动。作用域外渲染的内容(例如通过 portal 渲染的 toast 或对话框)不会移动,也不会被光带覆盖。 - 向动作方向靠拢。 对于按下的控件,要传入
at;否则页面会向origin属性所指的位置靠拢,默认值为顶部中央。at的单位是 CSS px,以框的左上角为原点,因此示例之所以有效,是因为它的框从视口左上角开始。对于其他框,要减去框自身的位置。对于不会移动的动作或无人按下控件触发的事件,应将origin设为框内的比例值,例如,底部的编辑区可以使用origin={[0.5, 1]}。 - 让光带有充足时间扫过整页。 上面的 650 到 900 ms 是针对
sweep()的。作用域的光带默认持续 1600 ms;扫过整页时,约 2700 ms 较为合适,因为光带到达另一侧前很久就会淡出。 - 每页只用少量几个,不要每一行都用一个。 每个作用域都会使用自己的 WebGL 上下文,而一个页面最多只能同时保留 16 个。
只有用户要求提供关闭效果的方法时,才添加 usePageEffect() 读取器开关。
其他配置
对于不使用 Next 的 React(Vite + react-router 等)、Next Pages Router 或原生 JS / 其他框架,请阅读
references/react-and-vanilla.md——尤其要注意,Pages Router 必须使用 glimm/react,而不是 glimm/next(Next 适配器基于 next/navigation 构建,仅适用于 App Router)。
变化应发生在扫掠过程中
扫掠会展示变化,因此可见变化绝不能先于扫带发生。把状态更新或导航放进传给
sweep() 的回调中,并确保所有受其影响的内容都位于扫掠区域内。glimm 随后会在扫带经过时运行它。
// Right: the new state appears behind the band
sweep(() => setPublished(true))
// Wrong: the new state is on screen before the band arrives
setPublished(true)
sweep(async () => {})
如果结果来自请求,请先等待请求完成,再在请求返回时调用 sweep()。按下的按钮可以立即显示加载状态,因为这是对按下操作的反馈。所有属于结果的内容,例如图例、计数或徽章,都应随扫掠一起变化。
扫掠绝不让任何人等待
动画跟随应用运行,因此应用绝不等待动画。
- 立即响应按下操作。 按下的控件应立即显示加载或选中状态,并且在扫掠之外。结果应放进扫掠中,其中也包括清除加载状态,这样旧页面就不会在扫带展示新页面之前短暂恢复空闲状态。
- 绝不添加延迟。 不要使用
setTimeout,也不要等待done或midpoint,来让变化与扫带同步,或让控件一直处于忙碌状态直到扫掠结束。sweep()会自行应用变化,并会在尽可能早的时机应用,同时确保变化仍不可见。 - 不要在回调中执行耗时操作。 回调完成前扫带无法开始,页面也会一直被阻塞。先等待请求完成,让页面保持可用,然后再调用
sweep()并传入结果。回调中可以返回路由变更,因为它正是要展示的变化。 - 仅在成功时扫掠。 调用
sweep()前先检查结果,包括响应状态,因为fetch遇到 500 时不会抛出错误。请求失败时不应扫掠,因为扫掠会向用户传达操作成功。应用如何处理失败由项目自行决定,因此不要改动这一部分。 - 允许新操作中断当前操作。 第二次调用
sweep()会替换正在播放的扫掠,因此不要排队等待扫掠,也不要在扫掠结束前禁用控件。浏览器支持 View Transitions 时,扫掠播放期间页面不会响应点击,这也是应用内扫掠应保持简短且少用的另一个原因。
根据变化范围设定尺寸,并从操作处开始
扫掠的范围应与变化大致相当,并且看起来应从触发变化的位置开始。
- 整个页面或视图都发生了变化: 使用
curve={0.12}的作用域包裹页面,或使用覆盖整个视口的 providersweep()。 - 某个位置发生了值得注意的变化: 使用作用域包裹页面,并设置
curve={0.22} span={0.64} fade={[0.16, 0.64]},这样扫带会停留在变化附近,并在页面中部之前消失。 - 单个元素发生了变化: 仅用作用域包裹该元素,并设置
curve={0.06} bandTight={34}。如果没有GlimmScope,请使用下方的容器方案。单张卡片的变化却使用整页扫掠,会让一个小瞬间显得比实际更重要。
选择一个能从操作处向外移动的 direction。例如,屏幕底部的编辑器应使用 btt 扫掠,顶部栏中的控件则应使用 ttb。侧边栏中的操作也可以让作用域使用 ltr,或使用 rtl。
在手机上,主要操作通常位于屏幕底部,因此即使桌面布局使用其他方向,btt 扫掠通常也更合适。扫掠不能依赖悬停,因此应从操作本身触发,并通过触摸进行检查。
在容器内扫过
GlimmScope 已经实现了这一切,因此这份做法适用于不包含它的版本。
当扫光在整个页面以外的单个元素内播放时(例如卡片、
图表或表格),光带必须在靠近该容器边缘时逐渐淡出——绝不要
让它止于生硬的边界。带有 createShader 的 feather 选项的 glimm 版本默认会这样处理。0.3.1 及更早版本没有
此选项,因此请先检查已安装的类型,再用 CSS 遮罩画布边缘。这份做法仅用于装饰性扫光,因此请在 onMidpoint 中进行更改,而不是通过 provider 的遮罩功能。做法见
references/react-and-vanilla.md。
第 3 步 — 有目的地选择调色板
别不加考虑地使用默认值。找到项目的品牌色 (CSS 自定义属性、Tailwind 配置、设计令牌),并选择色相最接近它们的预设:
| 预设 | 色相 | 给人的感觉 |
|---|---|---|
spectrum | 完整的一轮色相循环 | 同时呈现所有颜色——0.4 版起的默认值 |
prism | 青色 → 紫罗兰色 → 洋红色 | 虹彩、科技感——0.4 版之前的默认值 |
berry | 红粉色 → 洋红色 | 温暖、大胆、编辑风 |
lagoon | 蓝色 → 青色 → 绿色 | 清凉、平静、自然 |
citrus | 绿色 → 黄色 → 橙色 | 活力、俏皮 |
azure | 青色 → 蓝色 → 紫罗兰色 | 企业风、简洁、可靠 |
ember | 黄色 → 橙色 → 红粉色 | 炽热、紧迫、戏剧化 |
如需精确匹配品牌色,可使用品牌自身的十六进制颜色值构建自定义调色板:
import { accentChain } from 'glimm/next' // also exported from glimm/react and glimm
const brand = accentChain(['#7C3AED', '#EC4899']) // 2–4 stops, in sweep order
<GlimmProvider palette={brand}>
accentChain 会在 OKLCH 中插值各个色标(感知上均匀,不会产生浑浊的中间色),并使着色器调色板与之匹配。在深色网站上,为 provider 添加
brightness={0.85},避免光带在深色背景上过于明亮。更深入的调整——时序、缓动、方向、光带形状、效果配方——请参阅 references/tuning.md。
第 4 步 — 精选触发扫光的链接
扫光应标记场景变化,而不是每次点击。检查项目的
导航,并使用 data-glimm-skip 排除所有高频或场景内操作:
- 分页、表格排序、筛选器
- 页面内的标签页切换
- 页脚中的实用链接(隐私政策、条款)
- 会打开模态框或抽屉,而非进入新场景的操作
<Link href="/work?page=2" data-glimm-skip>Next page</Link>
主导航(页眉链接、进入详情页的卡片、醒目的 CTA)
保留扫光。如果筛选后只有两三个链接会触发扫光,
请重新考虑是否使用 InterceptLinks,改为只在这些链接上显式使用 <TransitionLink>——明确表达意图,胜过“全部拦截再逐个排除”。
步骤 5 — 验证效果
切勿交付未经验证的改动。启动开发服务器,实际点击进行两三次页面跳转,然后检查:
- 转场扫光正常播放,新页面出现在色带后面(没有未设置样式的内容闪现,也没有重复内容)。全速播放时往往太快,难以判断,因此可以暂时将
sweepMs设为约 5000,再观察一次。属于结果页面的内容,在色带扫到之前都不应可见。之后将sweepMs改回原值。 - 浏览器控制台没有问题——没有 glimm 警告,也没有 WebGL 错误。
- 按住 Cmd/Ctrl 并点击仍会在新标签页中打开链接,原页面不会播放转场扫光。
- 外部链接和锚点链接的行为与之前一致。
- 浏览器的前进/后退行为应与当前安装版本的文档一致——在当前版本中,这些操作会立即完成,不播放转场扫光;这是预期行为,不是 bug。
- 按下的控件会立即响应,不会在任何操作发生前暂停;转场扫光开始后紧接着按下其他控件也仍然有效。
- 在手机屏幕宽度下,以及所选的转场方向下,页面仍然易于阅读。
- 对于
GlimmScope,请求失败时不会播放转场扫光,页面保持原样。如果正在检查的浏览器不支持 HTML-in-Canvas,那么只显示普通扫光才是正确结果,并非 bug;页面只会在支持该功能的浏览器中移动。
如果发现问题,请先查看下面的“故障排除”,再修改接线方式。
步骤 6 — 简短自然地交付
最终回复应简短、口语化——几句话即可,不要写成报告。用户刚看过涉及两个文件的改动;不要加上标题和检查清单,照搬这份指南。只需说明:
- 改了什么(通常是
package.json和根布局),以及无法运行安装时要执行的安装命令; - 他们实际会调整的一两个参数(
palette、sweepMs——GlimmProvider的属性); - 一句话说明如何移除(删除 provider 和
InterceptLinks,执行npm uninstall glimm)——可撤销意味着可以放心尝试。
需要特别说明的事项用一句话带过,不要单独成节——“我无法在离线环境中运行,所以请在执行 npm install 后点击浏览几个页面。”就足够了。如果网站正在运行,询问是否想看看 lagoon 预设,比描述调色板更好。
故障排除
| 症状 | 原因 → 解决方法 |
|---|---|
useGlimm must be used inside <GlimmProvider> | 组件导入的入口点与 provider 不同,或者 provider 位于嵌套布局中。使用同一个入口点,并将其放在根布局中。 |
| 没有扫掠效果,立即导航,也没有错误 | 操作系统启用了减少动态效果,或者没有 WebGL(无头浏览器、某些虚拟机)。这是预期的回退行为,并非故障。 |
| 新内容出现后,效果带才在其上方播放 | 状态在扫掠回调之外发生了变化。将更新移入 sweep(() => ...)。 |
| 页面冻结,然后扫掠效果延迟播放 | 耗时操作位于扫掠回调中。先等待请求完成,再将结果传给 sweep()。 |
| 用户希望页面像 glimm.dev 演示一样弯曲或向内收拢 | 使用 GlimmScope。如果已安装的类型中没有它,请使用 npm install glimm@latest 升级,不要编写自定义着色器。 |
GlimmScope 播放了扫掠效果,但页面没有移动 | 浏览器不支持 HTML-In-Canvas(Chrome 157 及更高版本);这是预期的回退行为。也可能是读者通过 usePageEffect 将其关闭了。 |
GlimmScope 内什么都不显示,或者效果带远远超出屏幕 | 容器尺寸不合适。通过 style 设置高度,并让内容在容器内滚动。 |
| 扫掠效果播放完毕,但页面在结束时发生“跳动” | 使用的是较旧的 glimm(<0.3.1),没有 View-Transition 遮罩。请使用 npm install glimm@latest。 |
| 跳转到其他源的链接触发了异常的扫掠效果/出现故障 | 这些链接根本不应该被拦截——检查自定义的 shouldIntercept 是否漏掉了源检查。 |
| 卡片或图表中的扫掠效果以明显的硬边结束 | canvas 的 feather 为 0,或者 glimm 版本早于 feather 的引入版本。将 feather 保持为 'auto',或者在 CSS 中遮罩 canvas 边缘(参见 references/react-and-vanilla.md)。 |
| 深色主题下效果带太亮/褪色 | 在 provider 上设置 brightness={0.85}(或更低)。 |
TypeScript 找不到 glimm/next | moduleResolution 版本较旧。tsconfig 中需要设置 "bundler" 或 "node16"/"nodenext"——glimm 仅通过 exports 映射发布 ESM。 |
| React 18 StrictMode 中仅在开发环境下出现重复导航(非 Next) | 确保 navigate 回调是幂等的;provider 每次扫掠只会等待它执行一次,但应用级监听器如果被添加了两次,就会触发两次。 |