脉冲光环
光环以均匀的节奏从直播标识向外扩散,像信号从中心向外广播。
直播中
用纯 CSS 制作脉冲光环指示器。中央放一个带圆点和文字的小徽章。在它后面叠放三个大小相同、绝对定位的圆环(1.5px 主色边框,border-radius 50%)。每个圆环运行 3s 无限循环的 keyframe,从 scale(0.4)、opacity 0.8 到 scale(3)、opacity 0,缓动 cubic-bezier(0.16, 1, 0.3, 1),让它们先迅速扩开再缓缓飘散。用 animation-delay 0s、1s、2s 错开,让上一个淡出时总有新的开始。离开屏幕时暂停,在 prefers-reduced-motion 下只显示一个静态圆环。文字为“直播中”。
源代码
'use client'
import { useEffect, useRef } from 'react'
import s from './demo.module.css'
export default function PulseRings({ labels }: { labels: Record<string, string> }) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const el = ref.current
if (!el) return
const io = new IntersectionObserver(([e]) => el.toggleAttribute('data-paused', !e.isIntersecting))
io.observe(el)
return () => io.disconnect()
}, [])
return (
<div ref={ref} className={s.wrap}>
{[0, 1, 2].map((n) => (
<span key={n} className={s.ring} style={{ animationDelay: `${n}s` }} aria-hidden="true" />
))}
<span className={s.badge}>
<span className={s.dot} aria-hidden="true" />
{labels.label}
</span>
</div>
)
}
.wrap {
position: relative;
display: grid;
place-items: center;
width: 80px;
height: 80px;
}
.ring {
position: absolute;
inset: 0;
border: 1.5px solid var(--stage-accent);
border-radius: 50%;
opacity: 0;
animation: pulse 3s var(--ease-out) infinite;
}
.wrap[data-paused] .ring { animation-play-state: paused; }
@keyframes pulse {
from { transform: scale(0.4); opacity: 0.8; }
to { transform: scale(3); opacity: 0; }
}
.badge {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: var(--stage-accent);
color: var(--stage-accent-ink);
font: 600 var(--text-xs) / 1 var(--font-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
@media (prefers-reduced-motion: reduce) {
.ring { animation: none; }
.ring:first-child { opacity: 0.5; transform: scale(1.6); }
}