Skip to content

FreeIn reviewv1.0.0

Bokeh

Out-of-focus lights in three depths, brighter at the rim and drifting with the pointer.
npx shadcn@latest add @vibebox/bokeh

Controls

1
1
<Bokeh />

About

A lens draws a point of light as a disc, brighter along its edge. This scene lays three grids of them at three scales; every cell may hold a disc of its own radius and colour, which breathes on its own period and sways about its centre, and the 3x3 neighbourhood is checked so a disc is never cut at a cell edge. The nearer the layer, the larger the discs and the further the pointer moves them.

Use it when

  • A restaurant, bar, cafe, florist or boutique wants warmth behind the booking.
  • A wedding, party or event page wants the look of celebration.
  • A spa or wellness page wants the fresh dew look.

Skip it when

  • The brand is austere and technical.
  • The page is dense with small text over the discs.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/bokeh
  3. 3Use it:
    import { Bokeh } from "@/components/vibebox/bokeh";
    
    <Bokeh preset="candle" className="min-h-[560px] rounded-xl">
      <div className="p-12 text-white">
        <h1 className="text-4xl font-semibold">Dinner, slowly.</h1>
      </div>
    </Bokeh>

Code

3 files
components/vibebox/bokeh/bokeh.client.tsx
/**
 * VibeBox Motion · Bokeh
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
"use client";

import type { ReactNode } from "react";
import { BackdropCanvas, type PosterSpec } from "@/lib/vibebox/backdrop-canvas";
import { bokehPiece, presets, type BokehPreset } from "./bokeh.shader";

export type BokehColor = "base" | "base2" | "a" | "b" | "c" | "d";
export type BokehValue = "density" | "size" | "soft" | "depth";

export type BokehProps = {
  /** What sits on the scene: a heading, a call to action, a whole section. */
  children?: ReactNode;
  /** A named look: `candle`, `party`, `cool`, `champagne`, `dew`. */
  preset?: BokehPreset;
  /** Overrides single palette colours with any CSS colour. */
  colors?: Partial<Record<BokehColor, string>>;
  /** Multiplies the preset's speed (1 keeps it, 0 holds the scene still). */
  speed?: number;
  /** Multiplies the preset's brightness. */
  intensity?: number;
  /** The pointer and touch move the scene. */
  interactive?: boolean;
  /** Accessible name; leave it out when the scene is only a background. */
  label?: string;
  /** Hold one still frame until this is false (for a gallery card). */
  still?: boolean;
  className?: string;
} & Partial<Record<BokehValue, number>>;

const POSTER: PosterSpec = { type: "linear", from: "base", to: "base2", angle: 180, blobs: [{ color: "a", at: [22, 30], size: 30 }, { color: "b", at: [66, 22], size: 28 }, { color: "c", at: [78, 70], size: 30 }, { color: "d", at: [38, 78], size: 26 }] };

export function Bokeh({ children, preset, colors, speed, intensity, interactive = true, label, still, className, density, size, soft, depth }: BokehProps) {
  return (
    <BackdropCanvas
      slug="bokeh"
      piece={bokehPiece}
      presets={presets}
      fallback="candle"
      preset={preset}
      colors={colors}
      values={{ density, size, soft, depth }}
      speed={speed}
      intensity={intensity}
      poster={POSTER}
      scale={0.5}
      stillTime={2.4}
      still={still}
      interactive={interactive}
      label={label}
      className={className}
    >
      {children}
    </BackdropCanvas>
  );
}
components/vibebox/bokeh/bokeh.shader.ts
/**
 * VibeBox Motion · Bokeh
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
import { compose, createScenePiece, type ScenePreset } from "@/lib/vibebox/engine/backdrops";

/*
 * Out-of-focus lights: discs of colour at three depths, brighter at their rim the way a real lens
 * draws them, drifting and breathing slowly. The nearer layers are larger and slide further with
 * the pointer, which is what gives the field its depth.
 */

const SCENE = /* glsl */ `
uniform vec3 uBase;
uniform vec3 uBase2;
uniform vec3 uA;
uniform vec3 uB;
uniform vec3 uC;
uniform vec3 uD;
uniform float uDensity;
uniform float uSize;
uniform float uSoft;
uniform float uDepth;
uniform float uSpeed;
uniform float uIntensity;

float T;

vec3 layer(vec2 uv, float scale, float seed, float depth, vec2 par) {
  vec2 p = (uv + par * depth) * scale;
  p.y += T * 0.012 * (0.5 + depth) * scale;
  ivec2 id = ivec2(floor(p)) + ivec2(int(seed) * 71, int(seed) * 29);
  vec3 sum = vec3(0.0);
  // Check the neighbours too: a disc can overlap its cell's edge.
  for (int y = -1; y <= 1; y++) {
    for (int x = -1; x <= 1; x++) {
      ivec2 c = id + ivec2(x, y);
      float live = h2(c + ivec2(5, 1));
      if (live > uDensity) continue;
      vec2 o = h22(c);
      vec2 center = vec2(float(x), float(y)) + 0.15 + o * 0.7 + 0.12 * vec2(sin(T * 0.3 + o.x * 20.0), cos(T * 0.27 + o.y * 20.0));
      float rad = (0.18 + 0.32 * h2(c + ivec2(9, 4))) * uSize;
      float d = length(fract(p) - center) / rad;
      float pick = h2(c + ivec2(2, 8));
      vec3 colr = pick < 0.25 ? lin(uA) : pick < 0.5 ? lin(uB) : pick < 0.75 ? lin(uC) : lin(uD);
      float breathe = 0.65 + 0.35 * sin(T * (0.4 + pick) + pick * 30.0);
      float disc = smoothstep(1.0, 1.0 - uSoft, d);
      float rim = 0.55 + 0.45 * smoothstep(0.45, 1.0, d);
      float halo = exp(-d * d * 1.6) * 0.18;
      sum += colr * (disc * rim + halo) * breathe;
    }
  }
  return sum;
}

void main() {
  vec2 uv = centred(gl_FragCoord.xy);
  T = uTime * uSpeed;
  vec2 par = (uPointer - 0.5) * vec2(-0.08, -0.05) * (0.4 + uHover * 0.9);
  vec3 col = mix(lin(uBase2), lin(uBase), smoothstep(-0.55, 0.45, uv.y));
  col += layer(uv, 2.2, 1.0, 0.2, par) * 0.42 * uDepth;
  col += layer(uv, 3.6, 2.0, 0.6, par) * 0.34;
  col += layer(uv, 6.0, 3.0, 1.0, par) * 0.26;
  col *= uIntensity;
  col *= 1.0 - 0.3 * smoothstep(0.5, 1.2, length(uv * vec2(0.85, 1.1)));
  fragColor = vec4(finish(col, gl_FragCoord.xy), 1.0);
}
`;

export const fragment = compose(SCENE);

export const PRESET_NAMES = ["candle", "party", "cool", "champagne", "dew"] as const;
export type BokehPreset = (typeof PRESET_NAMES)[number];

export const presets: Record<BokehPreset, ScenePreset> = {
  candle: {
    colors: { base: "#2a1308", base2: "#0c0503", a: "#ffb347", b: "#ff8a3d", c: "#ffd27a", d: "#ff6a2a" },
    values: { density: 0.55, size: 1, soft: 0.18, depth: 1, speed: 0.7, intensity: 1.1 },
  },
  party: {
    colors: { base: "#1a0a2e", base2: "#07030f", a: "#ff3fa4", b: "#38d4ff", c: "#ffd23f", d: "#8d5bff" },
    values: { density: 0.5, size: 1.05, soft: 0.14, depth: 1, speed: 1, intensity: 1.1 },
  },
  cool: {
    colors: { base: "#0a2238", base2: "#030a14", a: "#4fd1ff", b: "#7b8cff", c: "#c9f1ff", d: "#2fe3c1" },
    values: { density: 0.5, size: 0.95, soft: 0.2, depth: 1, speed: 0.6, intensity: 1.05 },
  },
  champagne: {
    colors: { base: "#2b2118", base2: "#0d0a07", a: "#ffe6b0", b: "#f2c879", c: "#fff3d6", d: "#e7b4a0" },
    values: { density: 0.45, size: 0.95, soft: 0.16, depth: 1, speed: 0.5, intensity: 1.1 },
  },
  dew: {
    colors: { base: "#8fd0a2", base2: "#2f7d5a", a: "#f4fff0", b: "#c8f2b8", c: "#fff7c4", d: "#9de6d2" },
    values: { density: 0.5, size: 1.1, soft: 0.22, depth: 0.8, speed: 0.5, intensity: 0.8 },
  },
};

export const bokehPiece = createScenePiece("bokeh", fragment);
components/vibebox/bokeh/index.tsx
/**
 * VibeBox Motion · Bokeh
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
export { Bokeh, type BokehProps, type BokehColor, type BokehValue } from "./bokeh.client";
export { PRESET_NAMES as BOKEH_PRESETS, presets as bokehPresets, type BokehPreset } from "./bokeh.shader";

Props

PropTypeDefaultDescription
childrenReactNode—What sits on the scene: a heading, a call to action, a whole section.
preset"candle" | "party" | "cool" | "champagne" | "dew""candle"The look: candle, party, cool, champagne, dew.
colors{ base?: string; base2?: string; a?: string; b?: string; c?: string; d?: string }—Overrides single palette colours with any CSS colour: base (the background at the top), base2 (the background at the bottom), a (the first light colour), b (the second light colour), c (the third light colour), d (the fourth light colour).
speednumber1Multiplies the preset's speed; 0 holds the scene still.
intensitynumber1Multiplies the preset's brightness, 0.5 to 1.4.
densitynumber0.55How many lights fill the frame (0.15 to 0.9; each look sets its own).
sizenumber1How large the discs are (0.5 to 1.8; each look sets its own).
softnumber0.18How soft the disc edges are (0.05 to 0.6; each look sets its own).
depthnumber1How strong the nearest layer is (0 to 1.5; each look sets its own).
interactivebooleantrueThe pointer slides the layers, the near ones further.
labelstring—Accessible name; leave it out when the scene is only a background.
stillbooleanfalseHold one composed frame until false (a gallery card that plays on hover).
classNamestring—Merged with cn(); give it a size and a radius.

Accessibility and motion

Keyboard
Not focusable; content on it keeps its own focus order.
Screen readers
Decorative by default; with label it is an image with that name.
Touch
A finger moves the scene like the cursor does; scrolling is never blocked.
Reduced motion
One still frame of the lights; they do not drift or breathe.

Edge cases and limits

  • Light looks (dew) lose the additive glow: keep intensity at or below 1.
  • The largest discs are very bright; keep small text off them.
  • High density on a phone looks like noise; stay under 0.6.
  • Green and violet aurora curtains over a mountain ridge and stars

    Aurora

    Free

    Curtains of northern light over a night sky and a mountain ridge, drawn toward the pointer.

    Backgrounds · WebGL2

  • A dusk skyline of lit towers under an orange and violet sky

    A skyline of lit windows against a glowing sky, with a road of light trails in front.

    Backgrounds · WebGL2

  • A sea of white cloud under a peach sunrise sky

    A sea of cloud seen from above, or a ceiling of it seen from below, lit by a low sun.

    Backgrounds · WebGL2

↑ ↓ to move↵ to openesc to close