返回效果库

长按确认删除

Hold to Delete

按住不放,进度环走完才真正删除——松早一点就作废

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

实时预览

指针交互
按住 1.5 秒确认

按住不放,进度环走完才真正删除;中途松开、指针移出或按 Escape 都会取消并退回去。

键盘:Tab 聚焦 · 按住 Space / Enter · 松开取消

把指针移进这片区域触屏设备:直接在演示区域操作检测到「减少动态效果」,动效已降级

参数

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

角色

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

任务

实现一个长按确认删除按钮,按钮盒 216px × 52px。按下后 3px 的进度环在 1500ms 内线性填满,只有填满才触发一次删除;提前松开、指针移出按钮或用键盘取消,环在 180ms 内退回且不删除。按钮是真实的 <button>:按住 Space / Enter 开始计时,keyup 取消、Escape 作废;删除后变成绿色"已删除"帧,demo 提供撤销。reduced motion 下也不允许缩短倒计时。

文件契约

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

- 依赖:无。用 requestAnimationFrame 驱动进度,CSS transition 只负责取消时的回退;不要新增依赖。

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

实现要点

- 文件顶部加 "use client";受控/非受控都可用:deleted 由父组件传入,onConfirm 在环形进度填满的那一帧调用一次。

- 进度环是绝对定位在按钮盒上的 SVG <rect>,pathLength={100},stroke-dasharray: 100,用 stroke-dashoffset = 100 * (1 - progress) 表示进度;环不得参与布局,保证按钮的 216 × 52 盒尺寸在按住期间完全不变。

- 按下(onPointerDown)时记录 performance.now() 并启动一个 requestAnimationFrame 循环:progress = min(1, (now - start) / holdMs),每帧直接写 strokeDashoffset,不要经过 React state;进度到 1 时取消 rAF 并调用一次 onConfirm()。

- 取消路径:onPointerUp / onPointerLeave / onPointerCancel / onKeyUp(Space、Enter)/ Escape 都调用同一个 cancel():取消 rAF,给环加上 transition: stroke-dashoffset <resetMs>ms linear 并按 progress * resetMs 的剩余比例把 dashoffset 退回 100,过渡结束后移除内联 transition。

- 键盘:keydown 里对 Space / Enter 调 preventDefault() 开始计时(event.repeat 时忽略),keyup 取消;按钮始终保持原生 <button type="button">、可聚焦、不设 disabled。aria-label 写明需要按住几秒,另用 aria-live="polite" 播报开始 / 取消 / 已删除。

- prefers-reduced-motion: reduce:取消与删除的状态切换压到 ≤1ms,但 holdMs 计时不许缩短(安全要求),进度仍按真实时间推进;删除后的绿色终帧直接呈现。卸载或 deleted 变化时清掉 rAF、回退 timer 与事件监听。

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

- 按钮盒:216px × 52px,圆角 14px,左右内边距 16px,字号 13px;图标与文字间距 10px。盒尺寸锁死,任何阶段都不许动。

- 进度环:覆盖在按钮盒外 4px 处的 SVG,描边 3px(即 ringWidth 参数),pathLength 100,周长约 568px(用户单位),从顶部开始顺时针、圆头端点;用 stroke-dashoffset = 100 × (1 − progress) 表示进度,底环保持 rgba(201, 244, 90, 0.14)。

- 按住:pointerdown(或键盘 Space / Enter 的 keydown)后进度在 1500ms 内从 0 线性走到 1;只有填满才确认——进度精确到 1 的那一帧触发一次删除。

- 取消:提前松开、pointerleave、pointercancel、keyup 或 Escape 都会停止计时,并让环在最多 180ms 内按已显示的比例退回空心(与已推进的比例成正比),随后移除内联 transition。删除不会发生。

- 已删除帧:边框 rgba(74, 222, 128, 0.55)、背景 rgba(74, 222, 128, 0.12)、文字 #4ade80,显示对勾与"已删除 / Deleted";进度环隐去(dashoffset 100)。demo 提供"撤销",恢复 idle 并重新可按住。

- idle 视觉:边框 rgba(237, 238, 240, 0.16)、背景 rgba(237, 238, 240, 0.04)、文字 #edeef0;按住时边框 rgba(201, 244, 90, 0.55)、背景 rgba(201, 244, 90, 0.10);进度描边 #c9f45a,完成瞬间转 #4ade80。

- 只允许变化 stroke-dashoffset、transform、opacity、background-color、border-color、color;绝不允许动画 width、height、top、left、margin、padding。

行为

- 触发元素是真实的 <button type="button">;pointerdown / keydown 开始计时,pointerup / pointerleave / pointercancel / keyup / Escape 取消计时。

- 文字状态机:idle --pointerdown 或 keydown(Space/Enter)--> holding(进度在 1500ms 内线性 0 → 1,rAF 驱动)--> confirmed(进度精确到 1,onConfirm 只触发一次)--> deleted;holding --pointerup 或 pointerleave 或 pointercancel 或 keyup 或 Escape--> cancelling(环在 ≤180ms 内退回)--> idle;deleted --撤销--> idle;任意状态 --reduced motion--> 长按时长不变,仅状态切换压到 ≤1ms。

- 防重复进入:按住期间忽略后续 pointerdown / 重复 keydown(event.repeat);确认之后要等父组件复位才能再次按住。绝不设置 disabled——那会丢掉焦点。

- 进度用唯一的 requestAnimationFrame 循环直接写 stroke-dashoffset;不要在 React state 里每帧更新进度,也不要链式 setTimeout。

- 取消路径:停掉 rAF,给环加 transition: stroke-dashoffset 180ms linear,按已显示的比例把环退回空心,回退结束后移除内联 transition。

- 键盘与无障碍:按钮可聚焦;aria-label 写明需要按住几秒;aria-live="polite" 播报开始 / 取消 / 已删除;focus-visible 描边 2px #C9F45A、外扩 3px;手势过程中可访问名称不改变。

- prefers-reduced-motion:holdMs 保持 1500ms、进度按真实时间线性推进,只有取消 / 删除的视觉状态切换压到 ≤1ms;破坏性操作的等待不允许被缩短。

- 卸载时清理:取消 rAF、清掉回退 timer,销毁后不要有任何状态更新。

禁止

- 不要改变交互语义、可访问名称或删除真正触发的时机;这是同一个破坏性操作的视觉升级,不是新交互。

- 不要逐帧动画 width、height、top、left、margin 或 padding——按钮盒锁死,环是绝对定位覆盖层。

- 不要链式 setTimeout,也不要用 setInterval 推进进度;只允许一个 rAF 循环,松开与卸载时都要取消。

- 不要缩短、造假或跳过 1500ms 倒计时,reduced motion 下也一样;除非用户明确要求,进度也必须线性(不要缓动)。

- 不要用 disabled、pointer-events: none,也不要用 setPointerCapture 让 pointerleave / pointercancel 无法取消长按。

- 不要重复触发 onConfirm,也绝不能在环填满之前靠 pointerup 确认删除。

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

- 除 holdMs / resetMs 参数与文档默认值外不要硬编码按住或回退时长。

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

- 除 Motion 外不要引入任何动画库,更不要用 GSAP。

验收 —— 回答前逐项自查

1. 尺寸、进度与三个参数严格一致:按钮 216 × 52px、圆角 14px;进度环描边精确是 3px(可调参数时跟随 ringWidth),在 1500ms 内从空心线性填满,取消时在 180ms 内退回;删除终帧文案与绿色状态精确切换。

2. 按下后 60–80ms 采样:stroke-dashoffset 严格处于满值与该时刻应有值之间(既不是 0 也不是 100),按钮边框/背景处于过渡中的中间色,说明进度是连续推进而不是瞬跳。

3. 结束帧无残留:确认时 stroke-dashoffset 精确为 0;取消时精确回到满值,且回退过渡结束后内联 transition 已被移除,没有仍在运行的 rAF、timer 或过渡。

4. 整个按住、取消与删除过程中按钮父容器的 boundingBox 完全不变(216 × 52px),环只是绝对定位的覆盖层,不参与布局,页面不抖动、不出现滚动条跳动。

5. prefers-reduced-motion 下取消与删除的状态切换 ≤1ms;但长按计时 holdMs 不允许被缩短或跳过,进度仍按真实时间线性推进(破坏性操作的等待是安全语义,不是装饰,此条与通用验收的差异是有意为之)。

6. 键盘可完成完整操作:按钮可聚焦、Tab 后 focus-visible 为 2px #C9F45A、外扩 3px;按住 Space / Enter 在 keydown 开始计时、keyup 取消,Escape 直接作废;aria-label 写明需按住几秒,aria-live 播报开始 / 取消 / 已删除,且永不设置 disabled。

7. 零 console 报错;pointerup / pointerleave / pointercancel / 卸载后 rAF 与回退 timer 均已清理,没有残留的事件监听或指针捕获。

输出

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

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

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

与演示同源生成5,079 字符 · 1,411 约 tokens · 63 行
技术规格

一个 216px × 52px 的删除按钮,外面贴着一圈 3px 的进度环。按下后进度在 1500ms 内线性填满,填满的瞬间才触发删除;中途松开、指针移出(pointerleave / pointercancel)或用键盘提前抬键都会在 180ms 内把进度退回去,删除不会发生。键盘同样可完成:聚焦后按住 Space 或 Enter 开始计时,keyup 取消,Escape 直接作废。删除成功后按钮切到绿色的"已删除"帧,demo 里提供"撤销"恢复。长按计时在减少动态效果下也不会被缩短,安全相关的时长不允许变成装饰参数。

按住时长
1500 ms
回退时长
180 ms
进度环描边
3 px
按钮盒
216 × 52 px · r14
按住 → 删除
1500 ms · 1.5 s
取消回退
180 ms
环描边
3 px
环总周长
568 px
进度速度
66.7 %/s
键盘
Space / Enter 按住 · keyup 取消
验收清单

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

  1. 尺寸、进度与三个参数严格一致:按钮 216 × 52px、圆角 14px;进度环描边精确是 3px(可调参数时跟随 ringWidth),在 1500ms 内从空心线性填满,取消时在 180ms 内退回;删除终帧文案与绿色状态精确切换。
  2. 按下后 60–80ms 采样:`stroke-dashoffset` 严格处于满值与该时刻应有值之间(既不是 0 也不是 100),按钮边框/背景处于过渡中的中间色,说明进度是连续推进而不是瞬跳。
  3. 结束帧无残留:确认时 `stroke-dashoffset` 精确为 0;取消时精确回到满值,且回退过渡结束后内联 `transition` 已被移除,没有仍在运行的 rAF、timer 或过渡。
  4. 整个按住、取消与删除过程中按钮父容器的 boundingBox 完全不变(216 × 52px),环只是绝对定位的覆盖层,不参与布局,页面不抖动、不出现滚动条跳动。
  5. prefers-reduced-motion 下取消与删除的状态切换 ≤1ms;但长按计时 holdMs 不允许被缩短或跳过,进度仍按真实时间线性推进(破坏性操作的等待是安全语义,不是装饰,此条与通用验收的差异是有意为之)。
  6. 键盘可完成完整操作:按钮可聚焦、Tab 后 focus-visible 为 2px #C9F45A、外扩 3px;按住 Space / Enter 在 keydown 开始计时、keyup 取消,Escape 直接作废;`aria-label` 写明需按住几秒,`aria-live` 播报开始 / 取消 / 已删除,且永不设置 `disabled`。
  7. 零 console 报错;pointerup / pointerleave / pointercancel / 卸载后 rAF 与回退 timer 均已清理,没有残留的事件监听或指针捕获。

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

提示词如何还原这个效果

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

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

React + Tailwind