# Bokeh

> Out-of-focus orbs of light drifting slowly upwards and swaying, like the background of a night photograph.

Category: Backgrounds · Free · Updated 2026-10-22 · Live demo: https://motionkitui.com/components/bokeh

## When to use it

Evening, events, music and hospitality heroes, and soft backdrops behind testimonials or calls to action.

## How it works

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.

## Installation

```bash
npx shadcn@latest add https://motionkitui.com/r/bokeh.json
```

Or copy `components/motionkit/bokeh.tsx` below. Requires react, motion and Tailwind CSS v4.

## Props

| 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. |

## Accessibility

- **Keyboard: Not applicable.** Decorative, nothing to focus.
- **Screen readers: Pass.** Every orb is hidden from assistive tech; only your content is read.
- **Reduced motion: Pass.** With reduced motion on, the orbs hold still at half strength.
- **Colour contrast: Partial.** Ivory text over the core of a cream orb can drop to 3.0:1. Keep headings large, or leave out light colours behind body text.

## Source

```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>
  );
}
```
