返回效果库

浮层退场

Toast Exit

进来时滑得进来,走的时候先停住再离开

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

实时预览

布局

NOTIFICATION CENTER

通知中心

220ms in · 190ms out · 16px · 2600ms hold

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

参数

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

角色

你是一名资深前端工程师,工作在现有的项目中,技术栈是 React 19 + TypeScript + Tailwind CSS v4,进出场用纯 CSS keyframes,退场由 animationend 卸载。只实现下面这一个微交互:不要加额外功能,不要顺手重构周围代码。

任务

实现浮层堆栈:toast 从内侧 16px 处(translateX(-16px))用 220ms 淡入,停留 2600ms 后沿同一轴滑向 translateX(+16px) 并用 190ms 淡出,只有 animationend 之后才卸载。手动关闭走同一条退场路径。toast 按固定槽位堆叠,谁都不会被推着位移;同一段文案还在屏上时不会重复入栈。

文件契约

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

- 依赖:无需额外依赖:keyframes 由组件内的 <style> 注入。

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

实现要点

- 文件顶部加 "use client";用 forwardRef 暴露 push(message, kind),让外部通过 ref 推送。

- 维持一份 useRef 的 toast 列表作为唯一事实源,commit() 时同步 setToasts,让定时器永远读到最新栈而不是旧渲染快照。

- 退场用 data-exiting 切换 toast-exit-out keyframes;退场开始的那一刻把文案存进 snapshot 并冻结渲染,之后触发方清空数据也不会让飞出中的 toast 变空。

- 退场时 pointer-events: none,animationend(animationName === "toast-exit-out")后从数组移除;另设 exitMs + 400ms 兜底定时器防止动画事件不触发。

- 槽位按空闲索引分配(bottom = inset + slot × (height + gap)),删除一条不会重排其它 toast;同文案还在栈中时直接忽略;卸载时清空所有 hold / exit 定时器。

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

- 入场:从 translateX(-16px)、opacity 0 开始,到 translateX(0)、opacity 1 结束——时长 220ms,缓动 cubic-bezier(0.16, 1, 0.3, 1),animation-fill-mode: both。

- 退场:从 translateX(0)、opacity 1 开始,到 translateX(+16px)、opacity 0 结束——时长 190ms,缓动 cubic-bezier(0.4, 0, 1, 1),animation-fill-mode: both。

- 停留:入场结束后停留 2600ms 自动退场;手动关闭立刻开始退场,不等停留结束。

- 几何:每条 toast 宽 280px、高 44px、圆角 12px、左右内边距 12px,固定在右下角、距边 12px。槽位固定:bottom = 12px + slot × 52px,同时最多 3 条。

- 退场期间 toast 必须带 data-exiting="true" 且 pointer-events: none;只有 animationend 之后才从 DOM 移除。

- 只允许动画 transform 与 opacity;绝不允许动画 width、height、top、left、margin 或堆叠位置。

行为

- 推送规则:空文本忽略;同一段文案还在屏上时忽略(不允许重复入栈);栈满时忽略。

- 槽位分给最小空闲索引。删除一条时绝不允许重排其它 toast——别的 toast 在入场 / 退场期间 boundingBox 必须完全不变。

- 退场从快照开始:退场那一刻把当前的文案冻结进 snapshot 字段,之后 owner 清空数据也不会让飞出中的 toast 变空。

- 从 animationend(判断 event.animationName)里卸载节点,不要用估算的定时器;可以加一个比退场更长的兜底定时器,只作为不触发动画事件时的保险。

- 手动关闭按钮走与自动定时器完全相同的退场路径——不要立刻卸载。

- 可访问性:文案用 role="status"(错误用 role="alert");关闭控件是真实 <button type="button"> 且带 aria-label;focus-visible 显示 2px #C9F45A、外扩 3px 的描边。

- prefers-reduced-motion: reduce:不播入场动画也不播退场动画——toast 直接以最终态出现,退场时直接消失。

- 卸载时清理:清掉所有 setTimeout;行内 <style> 保持组件内作用域。

禁止

- 不要用布局属性(width、height、top、left、margin)来移动或堆叠 toast。

- 不允许推动别的 toast:已在屏上的 toast 不得重排、不得重算槽位。

- 不要用估算动画长度的 setTimeout 卸载,退场期间 pointer-events 不能还是可点的。

- 不允许同一文案重复堆叠;退场前和退场中都不允许把文案清空。

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

- 不要把组件包进页面、布局、演示壳或 Provider;不要改动全局 CSS、Tailwind 配置、package.json 或上面文件之外的任何文件。

- 不要引入任何动画库;进出场必须是纯 CSS keyframes。

验收 —— 回答前逐项自查

1. 入场时长、退场时长、位移距离、停留时间都与当前参数一致(默认 220ms / 190ms / 16px / 2600ms)。

2. 在入场与退场各自的 60–80ms 采样,toast 的 opacity 与 translateX 都严格处于起止状态之间,没有跳变或先闪出最终态。

3. 退场由 animationend 触发卸载:退场期间带 data-exiting、pointer-events:none,动画结束后节点才从 DOM 移除,不残留 transform。

4. 其它 toast 的 boundingBox 在整个入场 / 退场过程中完全不变,堆叠槽位固定,没有任何一条被推着位移。

5. 同一段文案还在屏上时不会重复入栈;退场前已把文案快照下来,退场过程中文案不会变空。

6. 推送按钮与关闭按钮都能 Tab 聚焦、Enter / Space 触发;关闭按钮有 aria-label,焦点环可见(2px lime,外扩 3px)。

7. 控制台 0 报错 / 警告;prefers-reduced-motion 下不播动画、直接最终态;卸载后没有残留定时器。

输出

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

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

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

与演示同源生成3,245 字符 · 901 约 tokens · 58 行
技术规格

右下角的浮动提示:入场从停靠点内侧 translateX(-16px) 处淡入并滑到位置(220ms,cubic-bezier(0.16, 1, 0.3, 1));停留 2600ms 后自动退场,或点关闭立刻退场。退场前先给当前文案拍快照,再沿同一轴滑向 translateX(+16px) 并淡出(190ms,cubic-bezier(0.4, 0, 1, 1)),期间 pointer-events:none 且带 data-exiting,animationend 之后才真正卸载。最多 3 条垂直堆叠,每条占据固定槽位,谁也不会把别人推着位移;同一段文案还在屏上时不会重复入栈。

入场时长
220 ms
退场时长
190 ms
位移距离
16 px
停留时长
2600 ms
入场 / 退场位移
-16px → 0 → +16px
单条生命周期
3010ms
堆叠上限 / 槽距
3 / 52px
缓动
cubic-bezier(0.16, 1, 0.3, 1) · cubic-bezier(0.4, 0, 1, 1)
验收清单

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

  1. 入场时长、退场时长、位移距离、停留时间都与当前参数一致(默认 220ms / 190ms / 16px / 2600ms)。
  2. 在入场与退场各自的 60–80ms 采样,toast 的 opacity 与 translateX 都严格处于起止状态之间,没有跳变或先闪出最终态。
  3. 退场由 animationend 触发卸载:退场期间带 data-exiting、pointer-events:none,动画结束后节点才从 DOM 移除,不残留 transform。
  4. 其它 toast 的 boundingBox 在整个入场 / 退场过程中完全不变,堆叠槽位固定,没有任何一条被推着位移。
  5. 同一段文案还在屏上时不会重复入栈;退场前已把文案快照下来,退场过程中文案不会变空。
  6. 推送按钮与关闭按钮都能 Tab 聚焦、Enter / Space 触发;关闭按钮有 aria-label,焦点环可见(2px lime,外扩 3px)。
  7. 控制台 0 报错 / 警告;prefers-reduced-motion 下不播动画、直接最终态;卸载后没有残留定时器。

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

提示词如何还原这个效果

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

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

React + Tailwind + CSS keyframes