Docs
Glowing Bento
A border glowing effect that adapts to any container or card, as seen on Cursor's website.
Installation
Install dependencies
npm i motion clsx tailwind-merge
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Run the following command
It will create a new file glowing-bento.tsx
inside the components/mage-ui/bento-grid
directory.
mkdir -p components/mage-ui/bento-grid && touch components/mage-ui/bento-grid/glowing-bento.tsx
Paste the code
Open the newly created file and paste the following code:
"use client";
import { memo, useCallback, useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import { animate } from "framer-motion";
import { Box, Lock, Search, Settings, Sparkles } from "lucide-react";
interface GlowingEffectProps {
blur?: number;
inactiveZone?: number;
proximity?: number;
spread?: number;
variant?: "default" | "white";
glow?: boolean;
className?: string;
disabled?: boolean;
movementDuration?: number;
borderWidth?: number;
}
const GlowingEffect = memo(
({
blur = 0,
inactiveZone = 0.7,
proximity = 0,
spread = 20,
variant = "default",
glow = false,
className,
movementDuration = 2,
borderWidth = 1,
disabled = false, // Changed default to false so it's enabled by default
}: GlowingEffectProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const lastPosition = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number>(0);
const handleMove = useCallback(
(e?: MouseEvent | { x: number; y: number }) => {
if (!containerRef.current) return;
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const element = containerRef.current;
if (!element) return;
const { left, top, width, height } = element.getBoundingClientRect();
const mouseX = e?.x ?? lastPosition.current.x;
const mouseY = e?.y ?? lastPosition.current.y;
if (e) lastPosition.current = { x: mouseX, y: mouseY };
const center = [left + width * 0.5, top + height * 0.5];
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1]);
const inactiveRadius = 0.5 * Math.min(width, height) * inactiveZone;
if (distanceFromCenter < inactiveRadius) {
element.style.setProperty("--active", "0");
return;
}
const isActive =
mouseX > left - proximity &&
mouseX < left + width + proximity &&
mouseY > top - proximity &&
mouseY < top + height + proximity;
element.style.setProperty("--active", isActive ? "1" : "0");
if (!isActive) return;
const currentAngle = parseFloat(element.style.getPropertyValue("--start")) || 0;
let targetAngle =
(180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90;
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180;
const newAngle = currentAngle + angleDiff;
animate(currentAngle, newAngle, {
duration: movementDuration,
ease: [0.16, 1, 0.3, 1],
onUpdate: (value) => element.style.setProperty("--start", String(value)),
});
});
},
[inactiveZone, proximity, movementDuration]
);
useEffect(() => {
if (disabled) return;
const handleScroll = () => handleMove();
const handlePointerMove = (e: PointerEvent) => handleMove(e);
window.addEventListener("scroll", handleScroll, { passive: true });
document.body.addEventListener("pointermove", handlePointerMove, { passive: true });
return () => {
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
window.removeEventListener("scroll", handleScroll);
document.body.removeEventListener("pointermove", handlePointerMove);
};
}, [handleMove, disabled]);
return (
<>
<div
className={cn(
"pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity",
glow && "opacity-100",
variant === "white" && "border-white",
disabled && "!block"
)}
/>
<div
ref={containerRef}
style={
{
"--blur": `${blur}px`,
"--spread": spread,
"--start": "0",
"--active": "0",
"--glowingeffect-border-width": `${borderWidth}px`,
"--repeating-conic-gradient-times": "5",
"--gradient":
variant === "white"
? `repeating-conic-gradient(
from 236.84deg at 50% 50%,
var(--black),
var(--black) calc(25% / var(--repeating-conic-gradient-times))
)`
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
repeating-conic-gradient(
from 236.84deg at 50% 50%,
#dd7bbb 0%,
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
)`,
} as React.CSSProperties
}
className={cn(
"pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity",
glow && "opacity-100",
blur > 0 && "blur-[var(--blur)] ",
className,
disabled && "!hidden"
)}
>
<div
className={cn(
"glow",
"rounded-[inherit]",
'after:content-[""] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]',
"after:[border:var(--glowingeffect-border-width)_solid_transparent]",
"after:[background:var(--gradient)] after:[background-attachment:fixed]",
"after:opacity-[var(--active)] after:transition-opacity after:duration-300",
"after:[mask-clip:padding-box,border-box]",
"after:[mask-composite:intersect]",
"after:[mask-image:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]"
)}
/>
</div>
</>
);
}
);
GlowingEffect.displayName = "GlowingEffect";
const GlowingEffectDemo = () => {
return (
<ul className="grid grid-cols-1 md:grid-cols-12 gap-4">
{gridItems.map((item, index) => (
<GridItem key={index} {...item} />
))}
</ul>
);
};
interface GridItemProps {
area: string;
icon: React.ReactNode;
title: string;
description: React.ReactNode;
}
const GridItem = ({ area, icon, title, description }: GridItemProps) => {
return (
<li className={`min-h-[14rem] list-none ${area}`}>
<div className="relative h-full rounded-2.5xl border p-2 md:p-3">
<GlowingEffect spread={40} glow={true} disabled={false} proximity={64} inactiveZone={0.01} />
<div className="relative flex flex-col justify-between gap-6 p-6 border rounded-xl shadow-lg">
<div className="w-fit p-2 border rounded-lg border-gray-600">{icon}</div>
<h3 className="text-xl font-semibold">{title}</h3>
<p className="text-sm text-gray-500">{description}</p>
</div>
</div>
</li>
);
};
const gridItems: GridItemProps[] = [
{ area: "md:[grid-area:1/1/2/7]", icon: <Box />, title: "Do things the right way", description: "Running out of copy so I'll write anything." },
{ area: "md:[grid-area:1/7/2/13]", icon: <Settings />, title: "The best AI code editor ever.", description: "Yes, it's true. I'm not even kidding." },
{ area: "md:[grid-area:2/1/3/7]", icon: <Lock />, title: "You should buy Mage UI Pro", description: "It's the best money you'll ever spend." },
{ area: "md:[grid-area:2/7/3/13]", icon: <Sparkles />, title: "This card is also built by Cursor", description: "I'm not even kidding. Ask my mom." },
{ area: "md:[grid-area:3/1/4/13]", icon: <Search />, title: "Coming soon on Mage UI", description: "I'm writing the code as I record this." }
];
export { GlowingEffect };
export default GlowingEffectDemo;