A rotating conic-gradient core with a wide blurred halo, a crisp counter-rotating inner ring and fine noise; the whole origin leans slightly toward the pointer with a lerped tilt.
Preview
Conic Gradient
A slow-turning spectrum core with a soft halo.
Customize
13 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add conic-gradientWrites components/ui/conic-gradient.tsx and components/styles/conic-gradient.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { ConicGradient } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <ConicGradient />6 );7}Props
Accessibility
Glow, ring and noise layers are aria-hidden with pointer-events: none, so the pointer tilt never blocks interaction with children. Rotation pauses off-screen or at speed 0; prefers-reduced-motion stops rotation and disables the pointer tilt.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import styles from "../styles/conic-gradient.module.css";5 6export interface ConicGradientProps {7 /** First conic stop color */8 color1?: string;9 /** Second conic stop color */10 color2?: string;11 /** Third conic stop color */12 color3?: string;13 /** Fourth conic stop color */14 color4?: string;15 /** Surface color behind the glow */16 backgroundColor?: string;17 /** Rotation speed multiplier (1 = one turn per 14s, 0 = paused) */18 speed?: number;19 /** Blur radius of the outer glow, in px */20 blur?: number;21 /** Diameter of the conic core as a percentage of the component width */22 spread?: number;23 /** Tilt the gradient origin slightly toward the pointer */24 interactive?: boolean;25 /** Show the crisp counter-rotating inner ring */26 ring?: boolean;27 /** Overlay a fine static noise texture */28 noise?: boolean;29 /** Height of the background in px */30 height?: number;31 /** Corner radius in px */32 borderRadius?: number;33 /** Foreground content rendered centered on top */34 children?: React.ReactNode;35 /** Optional className merged onto the root */36 className?: string;37 /** Inline styles merged onto the root */38 style?: React.CSSProperties;39}40 41const BASE_TURN_S = 14;42const MAX_TILT_PX = 36;43 44function useReducedMotionPref() {45 const [reduced, setReduced] = React.useState(false);46 React.useEffect(() => {47 if (typeof window === "undefined" || !window.matchMedia) return;48 const mq = window.matchMedia("(prefers-reduced-motion: reduce)");49 const update = () => setReduced(mq.matches);50 update();51 mq.addEventListener("change", update);52 return () => mq.removeEventListener("change", update);53 }, []);54 return reduced;55}56 57function useInView(ref: React.RefObject<HTMLElement | null>) {58 const [inView, setInView] = React.useState(true);59 React.useEffect(() => {60 const el = ref.current;61 if (!el || typeof IntersectionObserver === "undefined") return;62 const io = new IntersectionObserver((entries) => {63 const entry = entries[entries.length - 1];64 if (entry) setInView(entry.isIntersecting);65 }, { rootMargin: "80px" });66 io.observe(el);67 return () => io.disconnect();68 }, [ref]);69 return inView;70}71 72export function ConicGradient({73 color1 = "#ff3d7f",74 color2 = "#ffb020",75 color3 = "#18d6a3",76 color4 = "#5b6cff",77 backgroundColor = "#07070b",78 speed = 1,79 blur = 60,80 spread = 55,81 interactive = true,82 ring = true,83 noise = true,84 height = 360,85 borderRadius = 20,86 children,87 className = "",88 style,89}: ConicGradientProps) {90 const rootRef = React.useRef<HTMLDivElement>(null);91 const reduced = useReducedMotionPref();92 const inView = useInView(rootRef);93 const safeSpeed = Math.max(0, speed);94 const running = !reduced && safeSpeed > 0 && inView;95 96 React.useEffect(() => {97 const el = rootRef.current;98 if (!el || !interactive || reduced) return;99 const target = { x: 0, y: 0 };100 const cur = { x: 0, y: 0 };101 let raf = 0;102 const step = () => {103 cur.x += (target.x - cur.x) * 0.08;104 cur.y += (target.y - cur.y) * 0.08;105 el.style.setProperty("--mf-cg-tx", `${cur.x.toFixed(2)}px`);106 el.style.setProperty("--mf-cg-ty", `${cur.y.toFixed(2)}px`);107 if (Math.abs(target.x - cur.x) > 0.05 || Math.abs(target.y - cur.y) > 0.05) {108 raf = requestAnimationFrame(step);109 } else {110 raf = 0;111 }112 };113 const kick = () => {114 if (!raf) raf = requestAnimationFrame(step);115 };116 const onMove = (e: PointerEvent) => {117 const rect = el.getBoundingClientRect();118 const nx = ((e.clientX - rect.left) / rect.width) * 2 - 1;119 const ny = ((e.clientY - rect.top) / rect.height) * 2 - 1;120 target.x = Math.max(-1, Math.min(1, nx)) * MAX_TILT_PX;121 target.y = Math.max(-1, Math.min(1, ny)) * MAX_TILT_PX;122 kick();123 };124 const onLeave = () => {125 target.x = 0;126 target.y = 0;127 kick();128 };129 el.addEventListener("pointermove", onMove);130 el.addEventListener("pointerleave", onLeave);131 return () => {132 el.removeEventListener("pointermove", onMove);133 el.removeEventListener("pointerleave", onLeave);134 cancelAnimationFrame(raf);135 };136 }, [interactive, reduced]);137 138 const conic = `conic-gradient(from 0deg, ${color1}, ${color2}, ${color3}, ${color4}, ${color1})`;139 const styleVars = {140 "--mf-cg-bg": backgroundColor,141 "--mf-cg-blur": `${Math.max(0, blur)}px`,142 "--mf-cg-size": `${Math.max(5, spread)}%`,143 "--mf-cg-conic": conic,144 "--mf-cg-dur": `${(BASE_TURN_S / (safeSpeed > 0 ? safeSpeed : 1)).toFixed(2)}s`,145 "--mf-cg-state": running ? "running" : "paused",146 height: `${height}px`,147 borderRadius: `${borderRadius}px`,148 ...style,149 } as React.CSSProperties;150 151 return (152 <div ref={rootRef} style={styleVars} className={`${styles.root} ${className}`.trim()}>153 <div aria-hidden="true" className={styles.stage}>154 <div className={styles.glow} />155 <div className={styles.core} />156 {ring ? <div className={styles.ring} /> : null}157 <div className={styles.center} />158 </div>159 {noise ? <div aria-hidden="true" className={styles.noise} /> : null}160 <div aria-hidden="true" className={styles.vignette} />161 <div className={styles.content}>162 {children ?? (163 <div className={styles.copy}>164 <h3 className={styles.title}>Conic Gradient</h3>165 <p className={styles.subtitle}>A slow-turning spectrum core with a soft halo.</p>166 </div>167 )}168 </div>169 </div>170 );171}