A feathered light follows the cursor, revealing a glowing texture. On touch screens it drifts on its own and follows a drag.
Move through the light
Customize
Usage
import { CursorAtmosphere } from "@/components/motionkit/cursor-atmosphere";
<CursorAtmosphere
className="h-[32rem] rounded-3xl"
>
<h1>Your hero</h1>
</CursorAtmosphere>Motion
The exact curves this component runs, plotted with Motion's own maths.
Light follow
Pointer moves
Drift
No pointer, or a touch screen
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/cursor-atmosphere.tsx.
"use client";
import { useRef, type CSSProperties, type PointerEvent, type ReactNode } from "react";
import {
motion,
useAnimationFrame,
useMotionTemplate,
useMotionValue,
useReducedMotion,
useSpring,
} from "motion/react";
type CursorAtmosphereProps = {
children?: ReactNode;
/** Three colours for the light: centre, middle and outer edge. */
colors?: [string, string, string];
/** Radius of the light in pixels. */
radius?: number;
/** Strength of the light, 0 to 1. */
intensity?: number;
/**
* Follow the pointer. When off, and on touch screens until they are dragged across,
* the light drifts slowly on its own.
*/
interactive?: boolean;
/** The texture the light reveals. */
pattern?: "dots" | "grid" | "none";
className?: string;
};
function patternStyle(pattern: "dots" | "grid" | "none", color: string): CSSProperties {
if (pattern === "dots") return { backgroundImage: `radial-gradient(${color} 1px, transparent 1.5px)`, backgroundSize: "22px 22px" };
if (pattern === "grid")
return {
backgroundImage: `linear-gradient(${color} 1px, transparent 1px), linear-gradient(to right, ${color} 1px, transparent 1px)`,
backgroundSize: "44px 44px",
};
return {};
}
export function CursorAtmosphere({
children,
colors = ["#C084FC", "#6366F1", "#22D3EE"],
radius = 280,
intensity = 0.8,
interactive = true,
pattern = "dots",
className = "",
}: CursorAtmosphereProps) {
const ref = useRef<HTMLDivElement>(null);
const reduceMotion = useReducedMotion();
// true while the pointer (or a dragging finger) is steering the light
const steering = useRef(false);
const x = useMotionValue(-9999);
const y = useMotionValue(-9999);
const spring = { stiffness: 140, damping: 24, mass: 0.6 };
const smoothX = useSpring(x, spring);
const smoothY = useSpring(y, spring);
// the autonomous drift: a slow figure-of-eight that keeps the scene alive without input.
// With reduced motion the light rests in the middle instead.
useAnimationFrame((ms) => {
const element = ref.current;
if (!element || steering.current) return;
const { width, height } = element.getBoundingClientRect();
const t = reduceMotion ? 0 : ms / 1000;
x.set(width * (0.5 + Math.sin(t * 0.23) * 0.3));
y.set(height * (0.45 + Math.sin(t * 0.46) * 0.2));
});
function steer(event: PointerEvent<HTMLDivElement>) {
if (!interactive) return;
// a finger only steers while it is pressed, so ordinary scrolling never yanks the light around
if (event.pointerType !== "mouse" && event.buttons === 0) return;
const rect = event.currentTarget.getBoundingClientRect();
steering.current = true;
x.set(event.clientX - rect.left);
y.set(event.clientY - rect.top);
}
const lightX = reduceMotion ? x : smoothX;
const lightY = reduceMotion ? y : smoothY;
const mask = useMotionTemplate`radial-gradient(${radius}px circle at ${lightX}px ${lightY}px, black, rgb(0 0 0 / 0.55) 35%, transparent 100%)`;
const glow = useMotionTemplate`radial-gradient(${radius * 1.6}px circle at ${lightX}px ${lightY}px, ${colors[0]}, ${colors[1]} 30%, ${colors[2]} 55%, transparent 75%)`;
return (
<div
ref={ref}
onPointerMove={steer}
onPointerDown={steer}
onPointerLeave={() => (steering.current = false)}
onPointerUp={(event) => event.pointerType !== "mouse" && (steering.current = false)}
className={`relative isolate overflow-hidden bg-[#0B0A12] ${className}`}
>
{/* the texture at rest: barely there */}
<div aria-hidden className="absolute inset-0 -z-10 opacity-60" style={patternStyle(pattern, "rgb(255 255 255 / 0.07)")} />
{/* the light itself, a soft wash of colour */}
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 mix-blend-screen"
style={{ backgroundImage: glow, opacity: intensity * 0.35 }}
/>
{/* where the light falls, the texture glows in its colours */}
{pattern !== "none" && (
<motion.div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10"
style={{
...patternStyle(pattern, colors[0]),
maskImage: mask,
WebkitMaskImage: mask,
opacity: intensity,
}}
/>
)}
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| colors | [string, string, string] | ["#C084FC", "#6366F1", "#22D3EE"] | Centre, middle and outer colours of the light. |
| radius | number | 280 | Radius of the light in pixels. |
| intensity | number | 0.8 | Strength of the light, 0 to 1. |
| interactive | boolean | true | Follow the pointer. Off, it drifts on its own. |
| pattern | "dots" | "grid" | "none" | "dots" | The texture the light reveals. |
| children | ReactNode | — | Your content, layered above. |
You're on
Stage Light
A cone of light from above that sways gently and turns to follow the pointer.
Cursor Trail
A soft ribbon of light behind the pointer, across the window or inside one section.
Ambient by default
Aurora Background
Drifting, blurred color fields for hero sections and empty states.