You're on
Customize
Usage
import { StageLight } from "@/components/motionkit/stage-light";
<StageLight className="h-96 rounded-3xl pt-24 text-center">
<h1>Your hero</h1>
</StageLight>Motion
The exact curves this component runs, plotted with Motion's own maths.
Sway
Runs on its own
Aim
Pointer moves over the area
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/stage-light.tsx.
"use client";
import { useEffect, useState, type PointerEvent, type ReactNode } from "react";
import { animate, motion, useMotionValue, useReducedMotion, useSpring } from "motion/react";
type StageLightProps = {
children?: ReactNode;
/** Width of the light cone in degrees. */
width?: number;
/** How far the beam sways either side, in degrees. */
sway?: number;
/** Seconds for one sway from side to side. */
duration?: number;
/** Turn the beam toward the pointer while it is over the area. */
follow?: boolean;
/** Colour of the light. */
color?: string;
className?: string;
};
export function StageLight({
children,
width = 50,
sway = 12,
duration = 6,
follow = true,
color = "#5FAEB4",
className = "",
}: StageLightProps) {
const reduceMotion = useReducedMotion();
const [aiming, setAiming] = useState(false);
const rotate = useMotionValue(0);
const smoothRotate = useSpring(rotate, { stiffness: 60, damping: 18 });
// idle sway, paused while the beam is following the pointer
useEffect(() => {
if (reduceMotion || aiming) return;
const controls = animate(rotate, [-sway, sway], { duration, repeat: Infinity, repeatType: "mirror", ease: "easeInOut" });
return () => controls.stop();
}, [rotate, sway, duration, reduceMotion, aiming]);
function handlePointerMove(event: PointerEvent<HTMLDivElement>) {
if (!follow || reduceMotion) return;
const rect = event.currentTarget.getBoundingClientRect();
// angle from the light source (top centre) to the pointer; clockwise rotation swings the beam left
const dx = event.clientX - (rect.left + rect.width / 2);
const dy = Math.max(event.clientY - rect.top, 1);
const degrees = (-Math.atan2(dx, dy) * 180) / Math.PI;
rotate.set(Math.max(-45, Math.min(45, degrees)));
if (!aiming) setAiming(true);
}
return (
<div
onPointerMove={handlePointerMove}
onPointerLeave={() => setAiming(false)}
className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}
>
<motion.div
aria-hidden
className="absolute -inset-x-1/2 top-0 -z-10 h-[160%] opacity-40"
style={{
rotate: smoothRotate,
originY: 0,
// a cone pointing straight down from the top centre
background: `conic-gradient(from ${180 - width / 2}deg at 50% 0%, transparent, ${color} ${width / 2}deg, transparent ${width}deg)`,
maskImage: "linear-gradient(to bottom, black, transparent 70%)",
}}
/>
{/* the lamp itself */}
<div
aria-hidden
className="absolute left-1/2 top-0 -z-10 h-0.5 w-1/4 -translate-x-1/2 rounded-full"
style={{ backgroundColor: color, boxShadow: `0 0 30px 6px ${color}` }}
/>
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| width | number | 50 | Width of the light cone in degrees. |
| sway | number | 12 | How far the beam sways either side, in degrees. |
| duration | number | 6 | Seconds for one sway from side to side. |
| follow | boolean | true | Turn the beam toward the pointer while it is over the area. |
| color | string | "#5FAEB4" | Colour of the light. |
Move your cursor
Dot Glow Background
A quiet dot grid that lights up in teal around the pointer.
Move through the light
Cursor Atmosphere
A feathered light follows the cursor, revealing a glowing texture. On touch screens it drifts on its own and follows a drag.
Fast
Secure
Global
Typed
Spotlight Grid
One soft beam follows the pointer across a grid, lighting the borders and faces of the cards it passes.