返回效果库

骨架屏流光

Skeleton Shimmer

先占好位置,再让光扫过去

完整版:适合从零新建组件,规格、契约、验收全都在。

实时预览

布局
内容加载中

切换状态:骨架整棵卸载,真实内容占同一个盒子 · 1400ms

调整参数观察布局动效检测到「减少动态效果」,动效已降级

参数

边调边看,提示词同步更新。

角色

你是一名资深前端工程师,工作在现有的项目中,技术栈是 React 19 + TypeScript + Tailwind CSS v4(流光与骨架都用 CSS,不需要动画库)。只实现下面这一个微交互:不要加额外功能,不要顺手重构周围代码。

任务

实现骨架屏流光:内容加载期间渲染 8 个骨架块,尺寸与真实内容完全一致;一道 200% 宽的高光在 1400ms 内从 200% 0 扫到 -200% 0(linear、循环),每块比前一块晚 90ms 起扫。数据到达时必须整棵卸载骨架——不要假淡出——并且用 role=status / aria-busy 暴露加载状态、对辅助技术隐藏骨架块。

文件契约

- 只新建一个文件:components/effects/SkeletonShimmer.tsx。

- 依赖:无;流光由组件内注入的 CSS @keyframes 与 CSS 变量完成,不要引入动画库。

- 不要修改 package.json、全局 CSS、Tailwind 配置或任何其他已有文件。

实现要点

- 文件顶部加 "use client";导出 <SkeletonShimmer duration stagger sheen radius tone label />,props 默认值等于规格里的数值。

- 骨架固定 8 块:媒体 96px、标题 16px、正文 3 行 14px、页脚头像 28px + 两行 12/10px;所有尺寸放进常量,真实内容复用同一套。

- 组件内用 <style> 注入带前缀的 scoped CSS:每块 ::after 画 200% 宽的 linear-gradient(90deg, transparent, rgba(255,255,255,sheen), transparent),@keyframes 把 background-position 从 200% 扫到 -200%;animation: var(--sk-duration) linear infinite,animation-delay: i × stagger,animation-fill-mode: both。

- 明暗两套 alpha 通过 tone prop 切换 CSS 变量:dark 块 rgba(255,255,255,0.06)、sheen = sheen;light 块 rgba(9,11,16,0.09)、sheen = sheen × 2。

- 外层 role="status" aria-busy="true" aria-live="polite" + 视觉隐藏的「加载中」文案,每块 aria-hidden;加载完成由父级整个卸载骨架,不做假动画;prefers-reduced-motion 下 ::after 用 animation:none !important + opacity:0 !important。

规格 —— 精确数值,不许四舍五入或“优化”

- 高光带:每块的伪元素 ::after 铺满它(inset: 0),画 linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.08) 50%, transparent 100%),background-size: 200% 100%,background-repeat: no-repeat;把 background-position 从 200% 0 扫到 -200% 0,时长 1400ms、linear、infinite、animation-fill-mode: both。

- 逐块延迟:第 i 块(从 0 起)animation-delay: i × 90ms,8 块依次在 0 → 630ms 起扫,每块再来一轮 1400ms 的循环。不要随机延迟。

- 明暗两套 alpha,用 tone prop 切换:dark 块底 rgba(255, 255, 255, 0.06)、高光 rgba(255, 255, 255, 0.08);light 块底 rgba(9, 11, 16, 0.09)、高光 rgba(255, 255, 255, 0.16)——浅色底需要更强的白光才看得见。

- 圆角:每块 6px,媒体块 10px;头像为 28px 圆形。卡片圆角 16px、内边距 12px、1px 边框、行间距 12px。

- 结构:8 块,尺寸与真实内容一一对应——媒体 96px 高、满宽;标题 16px 高、宽 62%;正文 3 行、每行 14px 高、行距 8px、宽度 100% / 92% / 68%;页脚 28px 高 = 28px 头像 + 两行 12px / 10px、行距 4px。

- 真实内容必须复用同一套高度(标题行高 16px、正文行高 14px、头像 28px),这样骨架卸载、真实内容接管时不产生任何位移。

行为

- 状态机(文字状态图):

loading(role=status, aria-busy=true)—(数据到达,setLoaded(true))→ loaded(整棵骨架卸载,真实内容占同样的盒子)

loaded —(重新加载)→ loading(骨架重新挂载,每块 delay 从 0 重新计时)

loading | loaded —(卸载)→ teardown(注入的 <style> 随节点移除;不留下计时器或监听)

reduced-motion:loading → loading-static(::after 在 ≤1ms 内 animation:none + opacity:0,骨架静态可见)

- aria 约定:卡片 role=status + aria-busy=true + aria-live=polite,内含视觉隐藏的「加载中」文案;每个骨架块 aria-hidden。加载完成后整个 status 区域直接卸载。

- 只注入 scoped 样式:组件内写带前缀的 <style>,不要改全局 CSS、不要和框架类名冲突;keyframes 也放在里面。

- 只允许动画 background-position;整个加载过程中块的 width / height / top / left 与圆角必须保持不变。

- 卸载时注入的 <style> 随组件移除;不要留下定时器或 document/window 监听,卸载后不要更新状态。

禁止

- 不要改变交互语义、可访问名称(aria-label / role / aria-busy / 可见文案)或请求时机;只实现视觉动效。

- 不要动画 width / height / top / left;高光移动时骨架块的盒子必须固定。

- 不要用链式 setTimeout / setInterval 伪造流光——必须是 CSS 动画(或目标栈的补间引擎)。

- 不要新增依赖,也不要升级已安装的依赖。

- 不要改动全局 CSS、Tailwind 配置或上面文件之外的任何文件。

- 加载完成后不要把骨架留在 DOM 里,也不要用假过渡淡出——直接卸载。

- 不要留 TODO、不要写 // 其余代码、不要用 any、不要 console.log、不要留注释掉的实验代码。

- 不要引入任何动画库——GSAP、Motion 都不行。

验收 —— 回答前逐项自查

1. 滑动时长严格等于 duration(默认 1400ms):800ms / 2400ms 两端都能正常循环;逐块延迟严格等于 stagger(默认 90ms),第 8 块延迟 630ms。

2. 任取一块,60–80ms 时的 background-position 严格处于 200% 与 -200% 之间,说明高光真的在移动而不是静止。

3. 加载完成后骨架整棵卸载:DOM 里不残留骨架块、不残留进行中的 CSS 动画 / GSAP tween;深色 / 浅色两套 alpha 与规格一致。

4. 骨架与真实内容盒子尺寸一致:切换加载中 / 已加载前后卡片高度都是 260px,boundingBox 稳定,不抖动、不换行。

5. prefers-reduced-motion 时 ::after 的 animation 立即为 none、opacity 为 0,骨架静态可见(≤1ms 内生效),不出现位移。

6. 键盘可完成 Demo 里的全部操作(Tab 聚焦、Enter / Space 切换加载状态与明暗),焦点环清晰可见。

7. 0 console 报错;加载状态来回切换不抛错;卸载后没有残留的计时器、监听或注入的 <style>。

输出

- 用一个代码块输出完整文件内容,不要 diff、不要省略号,代码块之前不要解释。

- 如果缺少必要依赖,先停下来问我,不要擅自安装。

- 代码块之后最后输出一行:self-check: 1 ✓ · 2 ✓ · 3 ✓ · 4 ✓ · 5 ✓ · 6 ✓ · 7 ✓,未通过的项用 ✗ 并附一句原因。

与演示同源生成3,896 字符 · 1,082 约 tokens · 60 行
技术规格

数据还没回来时,用和真实内容同尺寸的 8 个骨架块先占住布局;一道 200% 宽的高光带以 1400ms 从右向左扫过(background-position 200% → -200%,循环),每块再延迟 90ms 依次起扫。深色 / 浅色两套 alpha,加载完成后整棵骨架直接卸载,真实内容占同一个盒子。

滑动时长
1400 ms
逐块延迟
90 ms
高光强度
0.08 α
圆角
6 px
高光渐变
linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent)
块序
8 块 · 0 → 630ms
验收清单

提示词要求 AI 在输出末尾逐条自检。

  1. 滑动时长严格等于 duration(默认 1400ms):800ms / 2400ms 两端都能正常循环;逐块延迟严格等于 stagger(默认 90ms),第 8 块延迟 630ms。
  2. 任取一块,60–80ms 时的 background-position 严格处于 200% 与 -200% 之间,说明高光真的在移动而不是静止。
  3. 加载完成后骨架整棵卸载:DOM 里不残留骨架块、不残留进行中的 CSS 动画 / GSAP tween;深色 / 浅色两套 alpha 与规格一致。
  4. 骨架与真实内容盒子尺寸一致:切换加载中 / 已加载前后卡片高度都是 260px,boundingBox 稳定,不抖动、不换行。
  5. prefers-reduced-motion 时 ::after 的 animation 立即为 none、opacity 为 0,骨架静态可见(≤1ms 内生效),不出现位移。
  6. 键盘可完成 Demo 里的全部操作(Tab 聚焦、Enter / Space 切换加载状态与明暗),焦点环清晰可见。
  7. 0 console 报错;加载状态来回切换不抛错;卸载后没有残留的计时器、监听或注入的 <style>。

待回归验证提示词与演示已同源生成;尚未在真实 Cursor / Claude Code 项目中做回归。

提示词如何还原这个效果

不是把效果描写一遍,而是把效果写成一份可执行的工程需求。

数字规格
每个像素、每个时间参数都来自同一个参数源——滑块改什么,提示词就写什么。
文件契约
只允许新建指定文件、指定依赖白名单,避免它顺手改你的全局样式或装一堆包。
验收清单
提示词末尾强制 AI 自检布局位移、键盘可用、reduced-motion 等 7 项。
完整版 / 中文

React + TypeScript