Bokeh Lights
Soft out-of-focus orbs of light drift and pulse behind the content, like city lights seen through a lens.
Evening edition
Create a bokeh light background in pure CSS. Place about ten absolutely positioned circles with random sizes (30–110px), positions, and warm hues set through inline CSS custom properties. Give each a radial-gradient fill that fades to transparent, filter: blur(6px), and mix-blend-mode: screen so overlapping orbs brighten. Animate each with a keyframe that drifts translate by ±20px and scales between 0.85 and 1.1 while opacity breathes between 0.35 and 0.8, using alternate infinite timing with a different duration (8–16s) and negative delay per orb so they never move in sync. Pause the animations when off screen. Under prefers-reduced-motion, keep the orbs still. The headline reads "Evening edition".
Source
'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; }
}