散景光斑
柔和的失焦光斑在内容后面飘动、呼吸,像透过镜头看到的城市灯光。
夜间特刊
用纯 CSS 制作散景光斑背景。放置约十个绝对定位的圆形,通过内联 CSS 自定义属性设置随机的尺寸(30–110px)、位置和暖色调。每个圆用渐变到透明的 radial-gradient 填充,加上 filter: blur(6px) 和 mix-blend-mode: screen,让重叠的光斑更亮。每个光斑运行一个 keyframe:translate 漂移 ±20px,scale 在 0.85 到 1.1 之间变化,opacity 在 0.35 到 0.8 之间呼吸;使用 alternate infinite,每个光斑的时长不同(8–16s)并设置负延迟,确保它们永远不同步。离开屏幕时暂停动画。在 prefers-reduced-motion 下光斑保持静止。标题为“夜间特刊”。
源代码
'use client'
import { useEffect, useRef } from 'react'
import s from './demo.module.css'
// Fixed values so server and client render the same markup.
const ORBS = [
[8, 20, 70, 40, 9], [22, 70, 40, 60, 12], [35, 30, 100, 30, 14], [48, 80, 55, 70, 10], [60, 15, 80, 50, 16],
[72, 60, 110, 25, 11], [85, 30, 45, 80, 13], [92, 75, 65, 45, 8], [15, 50, 35, 90, 15], [55, 45, 30, 20, 9],
]
export default function Bokeh({ 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.scene}>
{ORBS.map(([x, y, size, hue, dur], i) => (
<span
key={i}
className={s.orb}
aria-hidden="true"
style={{ '--x': `${x}%`, '--y': `${y}%`, '--size': `${size}px`, '--hue': hue, '--dur': `${dur}s`, '--delay': `${-i * 1.7}s` } as React.CSSProperties}
/>
))}
<p className={s.title}>{labels.title}</p>
</div>
)
}
.scene {
position: absolute;
inset: 0;
display: grid;
place-items: center;
overflow: hidden;
background: radial-gradient(ellipse at 50% 120%, oklch(28% 0.04 50), var(--stage-bg));
}
.orb {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--size);
height: var(--size);
margin: calc(var(--size) / -2) 0 0 calc(var(--size) / -2);
border-radius: 50%;
background: radial-gradient(circle, oklch(80% 0.13 var(--hue)) 0 40%, transparent 70%);
filter: blur(6px);
mix-blend-mode: screen;
opacity: 0.5;
animation: drift var(--dur) ease-in-out var(--delay) infinite alternate;
}
.scene[data-paused] .orb { animation-play-state: paused; }
@keyframes drift {
0% { transform: translate(-20px, 10px) scale(0.85); opacity: 0.35; }
50% { opacity: 0.8; }
100% { transform: translate(20px, -15px) scale(1.1); opacity: 0.45; }
}
.title {
position: relative;
margin: 0;
font: 600 var(--text-2xl) / 1 var(--font-display);
letter-spacing: -0.01em;
color: var(--stage-ink);
}
@media (prefers-reduced-motion: reduce) {
.orb { animation: none; }
}