返回效果库

弹窗入场

Modal Entrance

遮罩淡入,面板轻抬缩放;关闭比打开更快

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

实时预览

布局
卡片内弹出180 / 220 / 150 ms

Orbit 项目

遮罩与面板只在卡片范围内过渡,不会覆盖整个页面。

草稿已同步
等待评审 · 2

Esc 关闭 · Tab 在弹窗内循环 · 关闭后焦点回到「打开弹窗」

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

参数

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

角色

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

任务

实现弹窗入场:遮罩先用 180ms 线性淡入,同时面板从 translateY(8px)、scale(0.98) 与 opacity 0 过渡到最终态,用时 220ms,曲线 cubic-bezier(0.2, 0.8, 0.2, 1)。关闭时两者反向播放,时长压缩到 150ms;焦点必须在打开第一帧就进入面板。

文件契约

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

- 依赖:无需额外依赖。只用 React 与 CSS transition,不要引入动画库。

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

实现要点

- 文件顶部加 "use client"。导出 ModalEntrance,props 为 open、onClose、title、description、children、overlayMs、panelMs、closeMs、offset、scale、closeLabel、className。

- 状态机 rendered → active:open 变 true 时先以关闭态挂载,读一次 offsetWidth 强制 reflow,再切到开启态,过渡才会真正发生。

- 遮罩内联 transition 为 opacity 180ms linear,面板为 opacity/transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);关闭时两者都换成 closeMs。

- 关闭在面板的 transitionend 里卸载节点;打开后的第一帧用 panel.focus() 把焦点移进面板,并把 Esc / Tab 的 document 监听立即挂上。

- 遮罩点击只在 event.target === event.currentTarget 时关闭;关闭或卸载前把焦点还给打开前的 document.activeElement。

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

- 遮罩:用 position: absolute; inset: 0 铺满最近的定位祖先容器,背景 rgba(7, 8, 10, 0.62);opacity 0 → 1,用时 180ms,缓动 linear,不做位移与缩放。

- 面板:opacity 0 → 1、translateY(8px) → 0、scale(0.98) → 1,用时 220ms,曲线 cubic-bezier(0.2, 0.8, 0.2, 1);面板在遮罩内水平垂直居中。

- 关闭:完全反向播放同一组属性,遮罩与面板都改用 150ms——必须比打开更短。

- 面板视觉:最大宽度 300px,背景 #12151a,1px 边框 rgba(237, 238, 240, 0.1),圆角 18px,内边距 20px,阴影 0 24px 60px rgba(0, 0, 0, 0.55)。

- 作用域:遮罩绝对定位在宿主容器内部,禁止用 position: fixed 覆盖整页;Demo 在卡片里演示这一点。

- 焦点:打开后的第一帧就把焦点移进面板(面板 tabIndex={-1})并立即启用焦点陷阱;Esc 与点击遮罩关闭;关闭后焦点回到打开前的元素。

- ARIA:role="dialog"、aria-modal="true"、aria-labelledby 指向标题、aria-describedby 指向描述(这两个节点存在时)。

- prefers-reduced-motion: reduce 时两个时长都压到 ≤1ms,当帧直接到最终态,关闭立即卸载。

- 只允许动画 transform 与 opacity。

行为

- 状态机:closed --open=true--> mounting(提交关闭态:遮罩 opacity 0,面板 opacity 0 / translateY(offset) / scale(scale))--强制 reflow--> open(遮罩与面板过渡开始,焦点移入,陷阱生效)--open=false--> closing(同一组属性反向播放,时长用 closeMs)--面板 transitionend--> closed(卸载节点,焦点归还)。

- 打开:先渲染关闭态,读一次 offsetWidth 强制 reflow,再切到开启态,过渡才会真正发生;不允许跳过这次 reflow。

- 关闭:把遮罩与面板的过渡时长都换成 closeMs 并反向写目标值;在面板的 transitionend(opacity 或 transform)里卸载。点击遮罩只在 event.target === event.currentTarget 时关闭。

- 焦点:打开后的第一帧聚焦面板(早于动画结束);Tab 在面板内首尾包裹循环;Esc 关闭;关闭后把焦点还给打开前记录的元素。

- 关闭期间保持弹窗挂载以完整播放反向过渡;prefers-reduced-motion 生效时可以立即卸载。

- 任何状态下 ARIA 都要成立:role="dialog"、aria-modal="true"、由标题 labelled、由描述 described。

- 卸载时清理监听与未完成的过渡;不要在 document 上留下监听。

禁止

- 不要改变交互语义、可访问名称,也不要改变焦点进入面板的时机——必须第一帧就移入,不能等 transitionend。

- 不要用动画改变 width / height / top / left / margin / padding / box-shadow;只动 transform 与 opacity。

- 不要用链式 setTimeout 串打开 / 关闭时序;用过渡生命周期与 transitionend。

- 遮罩不要做 translate / scale 动画,面板不要用 max-height / top 过渡。

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

- 不要把组件包进页面、布局、演示壳或 Provider;不要让遮罩 fixed 覆盖整个文档。

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

- 不要引入任何动画库,CSS transition 足够。

验收 —— 回答前逐项自查

1. 遮罩 opacity 过渡 180ms linear,面板 opacity / transform 过渡 220ms cubic-bezier(0.2, 0.8, 0.2, 1);关闭两者都压缩到 150ms 并更快结束。

2. 打开后 60–80ms 采样:遮罩 opacity 严格介于 0 与 1 之间,面板 transform 处于 translateY(8px)/scale(0.98) 与归零态之间。

3. 过渡结束后面板 opacity 为 1、transform 归零,节点上没有残留的内联 left/top/height 或未清理的监听。

4. 打开、关闭、中途反向再打开时,页面容器 boundingBox 不抖动,卡片外内容不位移。

5. prefers-reduced-motion: reduce 时打开与关闭都 ≤1ms 且直接到最终态,不出现中间帧。

6. 键盘可以完成打开、面板内 Tab 循环与 Esc 关闭,关闭后焦点回到触发按钮,焦点环始终可见。

7. 控制台 0 报错或警告:无 React key/hydration 警告,无缺失 transition 目标,无未处理的 DOM 异常。

输出

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

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

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

与演示同源生成3,607 字符 · 1,002 约 tokens · 61 行
技术规格

打开时遮罩以 180ms 线性淡入,面板从 opacity 0、translateY(8px)、scale(0.98) 过渡到最终态,用时 220ms,曲线 cubic-bezier(0.2, 0.8, 0.2, 1)。关闭时同一组属性反向播放,但压缩到 150ms,比打开更短,打断感更轻。焦点在打开后的第一帧就进入面板并立即启用键盘陷阱,不等动画;Esc 或点击遮罩关闭后,焦点回到触发按钮。动画只改 transform 与 opacity,reduced-motion 下几乎瞬时。演示把弹窗放在卡片的绝对定位容器里,不会覆盖整站。

遮罩时长
180 ms
面板入场时长
220 ms
关闭时长
150 ms
起始位移
8 px
起始缩放
0.98 ×
打开总时长
220 ms
关闭 / 打开
68%
起始状态
8px · 0.98×
验收清单

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

  1. 遮罩 opacity 过渡 180ms linear,面板 opacity / transform 过渡 220ms cubic-bezier(0.2, 0.8, 0.2, 1);关闭两者都压缩到 150ms 并更快结束。
  2. 打开后 60–80ms 采样:遮罩 opacity 严格介于 0 与 1 之间,面板 transform 处于 translateY(8px)/scale(0.98) 与归零态之间。
  3. 过渡结束后面板 opacity 为 1、transform 归零,节点上没有残留的内联 left/top/height 或未清理的监听。
  4. 打开、关闭、中途反向再打开时,页面容器 boundingBox 不抖动,卡片外内容不位移。
  5. prefers-reduced-motion: reduce 时打开与关闭都 ≤1ms 且直接到最终态,不出现中间帧。
  6. 键盘可以完成打开、面板内 Tab 循环与 Esc 关闭,关闭后焦点回到触发按钮,焦点环始终可见。
  7. 控制台 0 报错或警告:无 React key/hydration 警告,无缺失 transition 目标,无未处理的 DOM 异常。

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

提示词如何还原这个效果

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

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

React + Tailwind