Out-of-focus orbs of light drifting slowly upwards and swaying, like the background of a night photograph.
Night lights
Customize
Usage
import { Bokeh } from "@/components/motionkit/bokeh";
<Bokeh className="h-[32rem] rounded-3xl">
<h1>Your hero</h1>
</Bokeh>Motion
The exact curves this component runs, plotted with Motion's own maths.
Drift
Runs on its own
Evening, events, music and hospitality heroes, and soft backdrops behind testimonials or calls to action.
Each orb is a radial gradient with a brighter offset core, blurred by its own amount and blended with screen so overlaps glow. Orbs drift upwards while swaying sideways, fading in at the start of the climb and out at the top. Their positions, sizes and timings come from an integer hash, so they land in exactly the same places on the server and in the browser, and negative delays start each one partway through its loop so they never move in step.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://motionkitui.com/r/bokeh.jsonnpm install motionSave as components/motionkit/bokeh.tsx.
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type BokehProps = {
children?: ReactNode;
/** Number of orbs. */
count?: number;
/** Colours the orbs are picked from, in turn. */
colors?: string[];
/** Seconds for an orb to drift its full path. Higher is calmer. */
duration?: number;
className?: string;
};
// an integer hash, so the orbs are scattered but land in exactly the same places on the server and in
// every browser (Math.sin can differ in its last digits between engines)
const hash = (n: number) => {
let x = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b);
x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35);
return ((x ^ (x >>> 16)) >>> 0) / 4294967296;
};
/** Out-of-focus orbs of light drifting slowly upwards and swaying, like the background of a night photo. */
export function Bokeh({ children, count = 18, colors = ["#5FAEB4", "#A56D5B", "#E4DCCF"], duration = 18, className = "" }: BokehProps) {
const reduceMotion = useReducedMotion();
const orbs = Array.from({ length: count }, (_, index) => ({
left: hash(index) * 100,
top: hash(index + 50) * 100,
size: 30 + hash(index + 100) * 90,
color: colors[index % colors.length],
blur: 4 + hash(index + 150) * 14,
delay: -hash(index + 200) * duration,
sway: (hash(index + 250) - 0.5) * 60,
seconds: duration * (0.7 + hash(index + 300) * 0.6),
}));
return (
<div className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
{orbs.map((orb, index) => (
<motion.span
key={index}
aria-hidden
className="absolute -z-10 rounded-full mix-blend-screen"
style={{
left: `${orb.left}%`,
top: `${orb.top}%`,
width: orb.size,
height: orb.size,
filter: `blur(${orb.blur}px)`,
background: `radial-gradient(circle at 35% 35%, ${orb.color}cc, ${orb.color}33 60%, transparent 70%)`,
}}
// a negative delay starts each orb partway through its loop, so they never move in step
animate={reduceMotion ? { opacity: 0.5 } : { y: [40, -120], x: [0, orb.sway, 0], opacity: [0, 0.75, 0.75, 0] }}
transition={
reduceMotion
? { duration: 0 }
: {
y: { duration: orb.seconds, delay: orb.delay, repeat: Infinity, ease: "linear" },
x: { duration: orb.seconds, delay: orb.delay, repeat: Infinity, ease: "easeInOut" },
// fade in at the start of the climb and out at the top, so orbs never pop
opacity: { duration: orb.seconds, delay: orb.delay, repeat: Infinity, times: [0, 0.2, 0.8, 1] },
}
}
/>
))}
<div aria-hidden className="absolute inset-0 -z-10 bg-[radial-gradient(ellipse_at_center,transparent_30%,rgb(12_16_16/0.7))]" />
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| count | number | 18 | Number of orbs. |
| colors | string[] | ["#5FAEB4", "#A56D5B", "#E4DCCF"] | Colours the orbs are picked from, in turn. |
| duration | number | 18 | Seconds for an orb's full drift. |
| children | ReactNode | — | Your content, layered above. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://motionkitui.com/r/bokeh.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the orbs hold still at half strength.