Appearance
总体思路
- 把“涨/跌”当成数据状态(
trend: 'up'|'down'),文本颜色直接用data-trend控制。 - 背景闪烁交给伪元素
::after只做opacity动画,避免影响布局与重绘范围。 - A/B 两套动画名(
cell-flash-a/cell-flash-b),每次变价时在-a/-b间切换,使浏览器认为是不同的animation-name→ 必重播。 - flip 不依赖时间,由“价格是否变化”驱动(每次变化
flip ^= 1),规避“时间戳没变导致不触发”的问题。 animation-fill-mode: both:首帧立即生效、尾帧保持透明,观感更自然。- 秒级时间戳统一处理:后端若给 10 位秒级时间戳,前端显示时务必
*1000,但不要用时间戳去决定动画 flip。
注意点
关键点就在这里: 浏览器只有在 computed style 中与动画相关的属性发生变化 时才会重播动画;最稳妥的做法就是让 animation-name 改变(比如 cell-flash-a ↔ cell-flash-b),因此我们用 A/B 两套 keyframes 来强制重播。
补充要点(伪元素同理适用):
仅仅再次添加同一个类名、或只改背景/颜色这类与动画无关的属性,不会重播。
可触发重播的还包括:
- 切换
animation-name(最稳); - 暂时设
animation: none→ 强制一次 reflow → 再恢复原动画; - 改变
animation-duration / delay / iteration-count等动画属性(多数浏览器会重启)。
- 切换
::after是否重播不取决于它是伪元素,而取决于它的 动画相关 computed style 是否变化。频繁 tick 的场景下,用
flip ^= 1切换-a/-b类名,是最简单、性能也稳的方案。
具体步骤
- 状态建模:为每行维护
bid/ask、trend与一个布尔flip(0/1)。每次价格变化,比较新旧值设置trend,并执行flip ^= 1。 - 类名策略:根据
trend与flip组合出四种类之一:tick-flash-up-a / tick-flash-up-b / tick-flash-down-a / tick-flash-down-b。 - 动画实现:提供两套
@keyframes(cell-flash-a/cell-flash-b),仅动画opacity,时长、透明度、缓动通过 CSS 变量可调。 - 颜色与可访问性:文本颜色用
data-trend控制;背景色在伪元素上设置;在@media (prefers-reduced-motion)内启用动画,尊重系统“减少动效”设置。 - 渲染细节:在切类名前可移除旧类名并强制一次 reflow(读
offsetWidth),确保快速连续更新仍然稳定重播。 - 时间展示:显示 HH:mm:ss 时,若后端是秒级
ts,格式化函数用new Date(ts * 1000);不要再用秒数奇偶去决定 flip。
最简可运行示例
纯原生 HTML/CSS/JS,完整复制到本地即可打开测试(点击按钮模拟“涨/跌”ticks)。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>报价闪烁最简示例</title>
<style>
:root {
--flash-ms: 900ms;
--flash-alpha: 0.18;
--flash-ease: cubic-bezier(0.22, 1, 0.36, 1);
--profit-rgb: 70, 205, 124;
--loss-rgb: 235, 72, 63;
}
.cell-quote {
position: relative;
display: inline-block;
padding: 6px 10px;
border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
isolation: isolate; /* 独立合成层,减少影响范围 */
}
.cell-quote::after {
content: "";
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
}
/* 文本着色由数据态驱动 */
.cell-quote[data-trend="up"] {
color: rgb(var(--profit-rgb));
}
.cell-quote[data-trend="down"] {
color: rgb(var(--loss-rgb));
}
/* 两套 keyframes:仅动画透明度,名字不同以强制重播 */
@keyframes cell-flash-a {
0%,
60% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes cell-flash-b {
0%,
60% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: no-preference) {
.tick-flash-up-a::after {
background: rgba(var(--profit-rgb), var(--flash-alpha));
/* 复杂:both 让首帧立即可见,结束保持透明 */
animation: cell-flash-a var(--flash-ms) var(--flash-ease) both;
}
.tick-flash-down-a::after {
background: rgba(var(--loss-rgb), var(--flash-alpha));
animation: cell-flash-a var(--flash-ms) var(--flash-ease) both;
}
.tick-flash-up-b::after {
background: rgba(var(--profit-rgb), var(--flash-alpha));
animation: cell-flash-b var(--flash-ms) var(--flash-ease) both;
}
.tick-flash-down-b::after {
background: rgba(var(--loss-rgb), var(--flash-alpha));
animation: cell-flash-b var(--flash-ms) var(--flash-ease) both;
}
}
button {
margin: 8px 6px;
padding: 6px 10px;
}
</style>
</head>
<body>
<div id="bid" class="cell-quote" data-trend="up">100.00</div>
<div style="margin-top:12px">
<button id="upBtn">模拟上涨 Tick</button>
<button id="downBtn">模拟下跌 Tick</button>
</div>
<script>
let flip = 0; // 复杂:A/B 轮换位
let price = 100.0;
// 复杂:应用一次“变价”并强制重播动画
function tick(direction) {
const el = document.getElementById("bid");
price += direction === "up" ? +0.01 : -0.01;
el.textContent = price.toFixed(2);
el.dataset.trend = direction;
// 复杂:根据方向与 flip 选择四种类名之一
const cls =
direction === "up"
? flip
? "tick-flash-up-b"
: "tick-flash-up-a"
: flip
? "tick-flash-down-b"
: "tick-flash-down-a";
// 复杂:先移除所有闪烁类,避免叠加;读 offsetWidth 触发一次 reflow,保证动画从 0% 开始
el.classList.remove(
"tick-flash-up-a",
"tick-flash-up-b",
"tick-flash-down-a",
"tick-flash-down-b"
);
void el.offsetWidth; // 强制 reflow
el.classList.add(cls);
// 复杂:flip ^= 1 使 0/1 互切,下一次必换 animation-name
flip ^= 1;
}
document.getElementById("upBtn").onclick = () => tick("up");
document.getElementById("downBtn").onclick = () => tick("down");
</script>
</body>
</html>若需要显示时间(HH:mm:ss),且后端返回秒级时间戳
ts(例如1761807859),请这样格式化:js// 复杂:秒级时间戳转毫秒 function formatHHmmss(tsSec) { const d = new Date(tsSec * 1000); const p = (n) => String(n).padStart(2, "0"); return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`; }
以上思路与示例即可平滑迁移到你的表格/组件里:把数据变更 → 设 trend → flip 翻转 → 切 A/B 类名这条链路打通,动画就会在每次价格变化时稳定触发,且可通过变量灵活调参。