成功对勾
圆环自己画合,对勾随之描出,徽章再轻轻弹一下,告诉你事情办妥了。
支付完成
用 SVG 和 CSS 制作成功徽章动画。在 64px 的 viewBox 中画一个圆(r 26)和一条对勾路径,都只有描边。每条路径的 stroke-dasharray 设为路径长度,stroke-dashoffset 设为同样的值。先用 600ms、cubic-bezier(0.65, 0, 0.35, 1) 把圆的 offset 动到 0,在 550ms 时开始用 300ms 把对勾的 offset 动到 0,在 450ms 时用 260ms 把一个实心圆盘从 scale 0 放大到 1 作为底色,让对勾画在它上面,最后在 850ms 时让整个徽章用 300ms 做 scale 1 → 1.1 → 1 的弹跳。使用 animation-fill-mode both,让它停在完成状态。添加一个“重播”按钮,用新的 key 重新挂载 SVG。在 prefers-reduced-motion 下直接显示完成后的徽章。说明文字为“支付完成”。
源代码
'use client'
import { useState } from 'react'
import s from './demo.module.css'
export default function SuccessCheck({ labels }: { labels: Record<string, string> }) {
const [run, setRun] = useState(0)
return (
<div className={s.wrap}>
<svg key={run} className={s.badge} viewBox="0 0 64 64" aria-hidden="true">
<circle className={s.disc} cx="32" cy="32" r="26" />
<circle className={s.ring} cx="32" cy="32" r="26" />
<path className={s.check} d="M21 33l7.5 7.5L44 25" />
</svg>
<p className={s.caption}>{labels.caption}</p>
<button type="button" className={s.replay} onClick={() => setRun((n) => n + 1)}>
{labels.replay}
</button>
</div>
)
}
.wrap {
display: grid;
justify-items: center;
gap: 10px;
}
.badge {
width: 72px;
height: 72px;
animation: pop 300ms var(--ease-out) 850ms both;
}
.ring, .check {
fill: none;
stroke: var(--stage-accent);
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.ring {
stroke-dasharray: 164;
stroke-dashoffset: 164;
transform: rotate(-90deg);
transform-origin: center;
animation: draw 600ms var(--ease-in-out) both;
}
.check {
stroke: var(--stage-accent-ink);
stroke-dasharray: 36;
stroke-dashoffset: 36;
animation: draw 300ms var(--ease-out) 550ms both;
}
.disc {
fill: var(--stage-accent);
transform: scale(0);
transform-origin: center;
animation: grow 260ms var(--ease-out) 450ms both;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
@keyframes grow { to { transform: scale(1); } }
@keyframes pop { 50% { transform: scale(1.1); } }
.caption {
margin: 0 0 6px;
font: 600 var(--text-base) / 1.2 var(--font-display);
color: var(--stage-ink);
}
.replay {
height: 32px;
padding: 0 12px;
border: 1px solid var(--stage-rule);
border-radius: var(--radius-sm);
background: var(--stage-surface);
color: var(--stage-ink);
font: 500 var(--text-sm) / 1 var(--font-body);
white-space: nowrap;
cursor: pointer;
}
.replay:hover { border-color: var(--stage-accent); }
.replay:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.badge, .ring, .check, .disc { animation: none; }
.ring, .check { stroke-dashoffset: 0; }
.disc { transform: none; }
}