nexus-labs
componentbackground-gradientsource: ok · hit
Background Gradient
Animated gradient wrapper with soft glow and blur for cards, buttons, and hero sections.
npx shadcn add "https://nexus-ui.com/r/background-gradient.json"free
Where it goes
hero
Visual style
glass-blurgradientpastel
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationframer-motion
Registry dependencies
https://www.nexus-ui.com/api/registry/nexus-fontutils
Files3 · source cached 10/9/2026, 10:16:30 PM
next-ui/background-gradient/background-gradient.tsx
"use client";
import * as React from "react";
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";
type BackgroundGradientAs = "div" | "button" | "section";
export type BackgroundGradientProps = {
children: React.ReactNode;
className?: string;
containerClassName?: string;
surfaceClassName?: string;
colors?: string[];
animationSpeed?: number;
blur?: number;
borderRadius?: string;
as?: BackgroundGradientAs;
interactive?: boolean;
disabled?: boolean;
};
const DEFAULT_COLORS = ["#22d3ee", "#818cf8", "#a855f7", "#ec4899"];
function clamp(n: number, min: number, max: number) {
return Math.max(min, Math.min(max, n));
}
export function BackgroundGradient({
children,
className,
containerClassName,
surfaceClassName,
colors = DEFAULT_COLORS,
animationSpeed = 1,
blur = 24,
borderRadius = "1rem",
as = "div",
interactive = true,
disabled = false,
}: BackgroundGradientProps) {
const reduceMotion = useReducedMotion() === true;
const [isHovered, setIsHovered] = React.useState(false);
const ref = React.useRef<HTMLElement | null>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const safeColors = colors.length >= 2 ? colors : DEFAULT_COLORS;
const speed = clamp(animationSpeed, 0.2, 3);
const blurPx = Math.max(0, blur);
const radialGlow = useMotionTemplate`radial-gradient(260px circle at ${x}px ${y}px, ${
safeColors[0]
}33 0%, transparent 70%)`;
const gradientMain = `linear-gradient(120deg, ${safeColors[0]} 0%, ${safeColors[1]} 35%, ${
safeColors[2] ?? safeColors[0]
} 68%, ${safeColors[3] ?? safeColors[1]} 100%)`;
const gradientSoft = `linear-gradient(300deg, ${safeColors[1]}66 0%, ${safeColors[2] ?? safeColors[0]}44 55%, ${
safeColors[0]
}66 100%)`;
const glowShadow = isHovered
? `0 20px 55px -24px ${safeColors[0]}99, 0 10px 30px -18px ${safeColors[2] ?? safeColors[1]}88`
: `0 16px 44px -24px ${safeColors[0]}7a, 0 8px 24px -16px ${safeColors[2] ?? safeColors[1]}66`;
function onMove(e: React.MouseEvent<HTMLElement>) {
if (!interactive || reduceMotion || !ref.current) return;
const rect = ref.current.getBoundingClientRect();
x.set(e.clientX - rect.left);
y.set(e.clientY - rect.top);
}
const commonProps = {
ref: ref as React.Ref<any>,
onMouseMove: onMove,
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
className: cn(
"group relative isolate inline-flex overflow-hidden border border-white/15 bg-black/85 text-white",
className,
),
style: { borderRadius, boxShadow: glowShadow },
};
const content = (
<>
<motion.span
aria-hidden
className="pointer-events-none absolute inset-[-35%]"
style={{
backgroundImage: gradientMain,
filter: `blur(${blurPx}px)`,
opacity: reduceMotion ? 0.55 : isHovered ? 0.86 : 0.72,
willChange: "transform, opacity, background-position",
}}
animate={
reduceMotion
? undefined
: {
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
rotate: [0, 6, -4, 0],
scale: isHovered ? 1.08 : 1,
}
}
transition={{ duration: 12 / speed, repeat: Infinity, ease: "easeInOut" }}
/>
<motion.span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ backgroundImage: gradientSoft, opacity: isHovered ? 0.48 : 0.3 }}
animate={reduceMotion ? undefined : { opacity: isHovered ? 0.52 : 0.34 }}
transition={{ duration: 0.25 }}
/>
{interactive && !reduceMotion ? (
<motion.span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ backgroundImage: radialGlow, opacity: isHovered ? 1 : 0 }}
transition={{ duration: 0.2 }}
/>
) : null}
<span
className={cn(
"absolute inset-[1px] rounded-[inherit] bg-background/72 dark:bg-black/48",
surfaceClassName,
)}
aria-hidden
/>
<div className={cn("relative z-10 w-full", containerClassName)}>{children}</div>
</>
);
if (as === "button") {
return (
<motion.button
type="button"
disabled={disabled}
{...commonProps}
whileTap={disabled ? undefined : { scale: 0.985 }}
>
{content}
</motion.button>
);
}
if (as === "section") {
return <motion.section {...commonProps}>{content}</motion.section>;
}
return <motion.div {...commonProps}>{content}</motion.div>;
}
next-ui/background-gradient/index.ts
export type { BackgroundGradientProps } from "./background-gradient";
export { BackgroundGradient } from "./background-gradient";
next-ui/background-gradient.tsx
/** Re-export shim: some bundlers resolve `@/components/background-gradient` here before directory index. */
export type { BackgroundGradientProps } from "./background-gradient/background-gradient";
export { BackgroundGradient } from "./background-gradient/background-gradient";