my/ui

Command Palette

Search for a command to run...

All components

Glowing Effect

borders

Proximity and scroll-aware conic gradient border glow using motion/react animate() for smooth angle transitions and requestAnimationFrame for debounced pointer tracking.

responsive · 600px

Install

Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:

$npx shadcn@latest add https://your-domain/r/inspira-glowing-effect.json

Usage

"use client";

import { GlowingEffect } from "@/registry/inspira-react/glowing-effect";

export default function GlowingEffectDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8 p-12 bg-neutral-950 min-h-[300px]">
      <div className="relative w-48 h-32 rounded-2xl border border-neutral-700 bg-neutral-900 flex items-center justify-center">
        <GlowingEffect disabled={false} spread={30} proximity={60} />
        <span className="text-white text-sm font-medium z-10">Hover around me</span>
      </div>
      <div className="relative w-48 h-32 rounded-2xl border border-neutral-700 bg-neutral-900 flex items-center justify-center">
        <GlowingEffect disabled={false} variant="white" spread={20} proximity={80} />
        <span className="text-white text-sm font-medium z-10">White variant</span>
      </div>
      <div className="relative w-48 h-32 rounded-2xl border border-neutral-700 bg-neutral-900 flex items-center justify-center">
        <GlowingEffect disabled={false} glow spread={40} borderWidth={2} />
        <span className="text-white text-sm font-medium z-10">Always glow</span>
      </div>
    </div>
  );
}

Component source

"use client";

import React, { useCallback, useEffect, useRef } from "react";
import { animate } from "motion/react";
import { cn } from "@/lib/utils";

interface GlowingEffectProps {
  blur?: number;
  inactiveZone?: number;
  proximity?: number;
  spread?: number;
  variant?: "default" | "white";
  glow?: boolean;
  className?: string;
  disabled?: boolean;
  movementDuration?: number;
  borderWidth?: number;
}

export function GlowingEffect({
  blur = 0,
  inactiveZone = 0.7,
  proximity = 0,
  spread = 20,
  variant = "default",
  glow = false,
  className,
  disabled = true,
  movementDuration = 2,
  borderWidth = 1,
}: GlowingEffectProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const lastPosition = useRef({ x: 0, y: 0 });
  const animationFrame = useRef(0);

  const gradient =
    variant === "white"
      ? `repeating-conic-gradient(from 236.84deg at 50% 50%, var(--black), var(--black) calc(25% / 5))`
      : `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%/5), #5a922c calc(50%/5), #4c7894 calc(75%/5), #dd7bbb calc(100%/5))`;

  const containerStyle: React.CSSProperties = {
    "--blur": `${blur}px`,
    "--spread": spread,
    "--start": "0",
    "--active": "0",
    "--glowingeffect-border-width": `${borderWidth}px`,
    "--repeating-conic-gradient-times": "5",
    "--gradient": gradient,
  } as React.CSSProperties;

  const handleMove = useCallback(
    (e?: MouseEvent | PointerEvent | { x: number; y: number }) => {
      if (!containerRef.current) return;

      if (animationFrame.current) {
        cancelAnimationFrame(animationFrame.current);
      }

      animationFrame.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 =
          Number.parseFloat(element.style.getPropertyValue("--start")) || 0;
        const 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 handlePointerMove = (e: PointerEvent) => handleMove(e);
    const handleScroll = () => handleMove();

    window.addEventListener("scroll", handleScroll, { passive: true });
    document.body.addEventListener("pointermove", handlePointerMove, { passive: true });

    return () => {
      if (animationFrame.current) cancelAnimationFrame(animationFrame.current);
      window.removeEventListener("scroll", handleScroll);
      document.body.removeEventListener("pointermove", handlePointerMove);
    };
  }, [disabled, handleMove]);

  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={containerStyle}
        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(
            "rounded-[inherit]",
            "after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))] after:rounded-[inherit]",
            "after:[mask:linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]",
            "after:bg-fixed after:[mask-clip:padding-box,border-box] after:[mask-composite:intersect]",
            "after:opacity-[var(--active)] after:transition-opacity after:duration-300 after:content-['']",
            "after:[background:var(--gradient)]",
            "after:[border:var(--glowingeffect-border-width)_solid_transparent]",
          )}
        />
      </div>
    </>
  );
}