组件 · COMPONENT
轻提示 Toast
深墨胶囊,从下方浮起,1.8 秒后消失。它不可点,所以可以是胶囊。文案说结果,不说过程:「已保存到你的笔记库」。
import { useRef, useState } from 'react'
import { Toast } from './extracted/Feedback.jsx'
import { Btn } from './components/v4/Button.jsx'
export default function ToastDemo() {
const [show, setShow] = useState(false)
const timer = useRef(null)
const fire = () => {
clearTimeout(timer.current)
setShow(false)
requestAnimationFrame(() => setShow(true))
timer.current = setTimeout(() => setShow(false), 1800)
}
return (
<div style={{ position: 'relative', width: '100%', minHeight: 112, display: 'grid', placeItems: 'center' }}>
<Btn onClick={fire}>保存笔记</Btn>
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
{show ? <Toast>✓ 已保存到你的笔记库</Toast> : null}
</div>
</div>
)
}<div style="position: relative; width: 100%; min-height: 112px; display: grid; place-items: center">
<button type="button" class="zzm-btn">保存笔记</button>
<div
style="
position: absolute;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
pointer-events: none;
"
></div>
</div>/* controls.css */
.zzm-v4 .zzm-btn {
-webkit-appearance: none;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 36px;
padding: 0 16px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
letter-spacing: .2px;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */
border: 1px solid rgba(0, 0, 0, .14);
color: #3F3F3B;
background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%);
box-shadow:
0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */
0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */
0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */
inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */
inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */
transition: box-shadow .16s ease, transform .16s ease, background .16s ease,
border-color .16s ease, filter .16s ease, opacity .16s ease;
}
.zzm-v4 .zzm-btn:hover {
transform: translateY(-1px);
border-color: rgba(0, 0, 0, .18);
box-shadow:
0 0 0 4px rgba(255, 255, 255, .8),
0 2px 4px rgba(0, 0, 0, .10),
0 4px 12px rgba(0, 0, 0, .09),
inset 0 1px 0 rgba(255, 255, 255, .95),
inset 0 -1px 0 rgba(0, 0, 0, .05);
}
.zzm-v4 .zzm-btn:active {
transform: translateY(0);
background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */
box-shadow:
0 0 0 3px rgba(255, 255, 255, .45),
0 1px 0 rgba(255, 255, 255, .65),
inset 0 2px 4px rgba(0, 0, 0, .13),
inset 0 1px 2px rgba(0, 0, 0, .09);
}
.zzm-v4 .zzm-btn:focus-visible {
outline: none;
box-shadow:
0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */
0 1px 2px rgba(0, 0, 0, .09),
0 2px 6px rgba(0, 0, 0, .06),
inset 0 1px 0 rgba(255, 255, 255, .92);
}
.zzm-v4 .zzm-btn:disabled {
cursor: not-allowed;
transform: none;
}
.zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) {
color: #A2A29C;
background: #F2F2EF;
border-color: rgba(0, 0, 0, .08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04);
}✓ 已保存到你的笔记库
import { Toast } from './extracted/Feedback.jsx'
export function Static() {
return <Toast>✓ 已保存到你的笔记库</Toast>
}<div role="status" class="zzm-toast">✓ 已保存到你的笔记库</div>/* extracted.css */
.zzm-v4 .zzm-toast {
display: inline-block; background: #1D1D1B; color: #FFFFFF; font-size: 12px;
padding: 9px 18px; border-radius: 999px; white-space: nowrap;
box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
animation: zzmUp .25s ease both;
}
/* keyframes */
@keyframes zzmUp { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }定位的坑
.zzm-toast 带 zzmUp 进场动画,动画结束帧是 transform: none——如果你用 left 50% 加 translateX(-50%) 居中,动画会把 transform 覆盖掉,提示就偏到右边去了。用外层 flex 居中:
正确的居中方式
<div style="position:fixed;left:0;right:0;bottom:24px;display:flex;justify-content:center;pointer-events:none">
<div class="zzm-toast" role="status">✓ 已保存到你的笔记库</div>
</div>铁律
✓ 要说结果:「已保存」「已复制」「已删除」
✗ 不要说过程或道歉:「操作成功!」「抱歉,出错了」
✓ 要1.8 秒自动消失;出错要让人处理时用确认窗或内联错误