Moving border
Animated gradient border that smoothly travels around any element, from rounded rectangles to perfect circles.
Perfect for buttons, cards, avatars, and interactive elements.
Live Playground · Edit and see changes instantly
Or edit with AI support by Open in
import {MovingBorder} from "@/components/phucbm/moving-border"; export default function Example() { return ( <div className="flex justify-center items-center flex-wrap h-screen gap-x-12 gap-y-6 bg-emerald-50"> <div className="flex flex-col gap-3 justify-center items-center"> {/* The radius prop should be identical with your rounded value */} <MovingBorder radius={10} borderWidth={2} gradientWidth={60} duration={3} colors={["#dce817", "#10f400", "#75ba33"]}> <button className="rounded-[10px] w-[100px] aspect-video bg-emerald-200 flex justify-center items-center transition-all duration-500 hover:bg-emerald-300"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="icon icon-tabler icons-tabler-outline icon-tabler-mood-sing"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/> <path d="M9 9h.01"/> <path d="M15 9h.01"/> <path d="M15 15m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/> </svg> </button> </MovingBorder> <div>Button</div> </div> <div className="flex flex-col gap-3 justify-center items-center"> {/* Circle */} <MovingBorder isCircle={true} borderWidth={4} gradientWidth={150} duration={4} colors={["#84b5ff", "#dad7f8", "#cb92ff"]}> <div className="w-[200px] aspect-square bg-accent rounded-full overflow-hidden flex justify-center items-center"> <img className="object-cover w-full h-full" src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExcGJrZ3NlejZ4ZXlvaDRnbTR1b2VmcG1waGM1Y3hvNGU4aGE0aHcweSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/26BkNnO2qmCWTQuac/giphy.gif" alt="A blurry photo of white flowers in a field"/> </div> </MovingBorder> <div>Avatar</div> </div> </div> ); }
Installation
pnpm dlx shadcn@latest add https://ui.phucbm.com/r/moving-border.jsonProps
Tweak the parameters
Live Playground · Edit and see changes instantly
Or edit with AI support by Open in
'use client'; import {MovingBorder} from "@/components/phucbm/moving-border"; import {useEffect, useRef, useState} from "react"; export default function MovingBorderExample_02() { const [isCircle, setIsCircle] = useState(true); const [radius, setRadius] = useState(10); const [borderWidth, setBorderWidth] = useState(4); const [gradientWidth, setGradientWidth] = useState(150); const [duration, setDuration] = useState(4); const [colors, setColors] = useState(["#84b5ff", "#dad7f8", "#cb92ff"]); const [newColor, setNewColor] = useState("#ff6b6b"); // Debounced values for rendering const [debouncedRadius, setDebouncedRadius] = useState(radius); const [debouncedBorderWidth, setDebouncedBorderWidth] = useState(borderWidth); const [debouncedGradientWidth, setDebouncedGradientWidth] = useState(gradientWidth); const [debouncedDuration, setDebouncedDuration] = useState(duration); // Debounce timers const radiusTimer = useRef<NodeJS.Timeout | null>(null); const borderWidthTimer = useRef<NodeJS.Timeout | null>(null); const gradientWidthTimer = useRef<NodeJS.Timeout | null>(null); const durationTimer = useRef<NodeJS.Timeout | null>(null); // Debounce radius useEffect(() => { if (radiusTimer.current) clearTimeout(radiusTimer.current); radiusTimer.current = setTimeout(() => { setDebouncedRadius(radius); }, 100); return () => { if (radiusTimer.current) clearTimeout(radiusTimer.current); }; }, [radius]); // Debounce border width useEffect(() => { if (borderWidthTimer.current) clearTimeout(borderWidthTimer.current); borderWidthTimer.current = setTimeout(() => { setDebouncedBorderWidth(borderWidth); }, 100); return () => { if (borderWidthTimer.current) clearTimeout(borderWidthTimer.current); }; }, [borderWidth]); // Debounce gradient width useEffect(() => { if (gradientWidthTimer.current) clearTimeout(gradientWidthTimer.current); gradientWidthTimer.current = setTimeout(() => { setDebouncedGradientWidth(gradientWidth); }, 100); return () => { if (gradientWidthTimer.current) clearTimeout(gradientWidthTimer.current); }; }, [gradientWidth]); // Debounce duration useEffect(() => { if (durationTimer.current) clearTimeout(durationTimer.current); durationTimer.current = setTimeout(() => { setDebouncedDuration(duration); }, 100); return () => { if (durationTimer.current) clearTimeout(durationTimer.current); }; }, [duration]); const handleAddColor = () => { if (colors.length < 10) { setColors([...colors, newColor]); } }; const handleRemoveColor = (index: number) => { if (colors.length > 1) { setColors(colors.filter((_, i) => i !== index)); } }; const handleColorChange = (index: number, value: string) => { const updatedColors = [...colors]; updatedColors[index] = value; setColors(updatedColors); }; return ( <div className="flex justify-center items-center flex-wrap h-screen bg-emerald-50"> <div className="w-[calc(100%-240px)] flex justify-center items-center"> {/* Moving Border Element */} <MovingBorder key={isCircle ? 'circle' : 'square'} isCircle={isCircle} {...(!isCircle && {radius: debouncedRadius})} borderWidth={debouncedBorderWidth} gradientWidth={debouncedGradientWidth} duration={debouncedDuration} colors={colors} > <div className="w-[200px] aspect-square bg-accent overflow-hidden flex justify-center items-center" style={{ borderRadius: isCircle ? '50%' : `${debouncedRadius}px` }} > <img className="object-cover w-full h-full" src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExcGJrZ3NlejZ4ZXlvaDRnbTR1b2VmcG1waGM1Y3hvNGU4aGE0aHcweSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/26BkNnO2qmCWTQuac/giphy.gif" alt="A blurry photo of white flowers in a field" /> </div> </MovingBorder> </div> {/* Control Panel */} <div className="w-[240px] p-3"> <div className="bg-white rounded-xl p-3 shadow-xl max-h-[95vh] overflow-y-auto custom-scrollbar"> {/* Shape Toggle Section */} <div className="mb-4"> <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2"> Shape </div> <div className="flex items-center justify-between"> <span className="text-sm text-gray-900 font-medium">Circle Mode</span> <button onClick={() => setIsCircle(!isCircle)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ isCircle ? 'bg-emerald-500' : 'bg-gray-200' }`} > <span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ isCircle ? 'translate-x-6' : 'translate-x-1' }`} /> </button> </div> </div> <div className="h-px bg-gray-200 my-3"/> {/* Parameters Section */} <div> <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2"> Parameters </div> {/* Radius Control - Only shown when not circle */} {!isCircle && ( <div className="mb-2"> <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium"> <span>Radius</span> <span className="font-semibold text-emerald-500">{radius}px</span> </label> <input type="range" min="1" max="50" step="1" value={radius} onChange={(e) => setRadius(Number(e.target.value))} className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" /> <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight"> Border radius for corners </p> </div> )} {/* Border Width Control */} <div className="mb-2"> <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium"> <span>Border Width</span> <span className="font-semibold text-emerald-500">{borderWidth}px</span> </label> <input type="range" min="1" max="50" step="1" value={borderWidth} onChange={(e) => setBorderWidth(Number(e.target.value))} className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" /> <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight"> Thickness of the border </p> </div> {/* Gradient Width Control */} <div className="mb-2"> <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium"> <span>Gradient Width</span> <span className="font-semibold text-emerald-500">{gradientWidth}px</span> </label> <input type="range" min="10" max="400" step="10" value={gradientWidth} onChange={(e) => setGradientWidth(Number(e.target.value))} className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" /> <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight"> Width of gradient effect </p> </div> {/* Duration Control */} <div className="mb-2"> <label className="flex justify-between text-[0.7rem] text-gray-900 mb-1 font-medium"> <span>Duration</span> <span className="font-semibold text-emerald-500">{duration}s</span> </label> <input type="range" min="1" max="20" step="0.5" value={duration} onChange={(e) => setDuration(Number(e.target.value))} className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer slider" /> <p className="text-[0.65rem] text-gray-400 mt-1.5 leading-tight"> Animation speed (lower=faster) </p> </div> </div> <div className="h-px bg-gray-200 my-3"/> {/* Colors Section */} <div> <div className="text-[0.7rem] font-semibold text-gray-400 uppercase tracking-wider mb-2"> Colors </div> {/* Color List */} <div className="space-y-2 mb-3"> {colors.map((color, index) => ( <div key={index} className="flex items-center gap-2"> <input type="color" value={color} onChange={(e) => handleColorChange(index, e.target.value)} className="w-8 aspect-square rounded cursor-pointer border border-gray-200" /> <input type="text" value={color} onChange={(e) => handleColorChange(index, e.target.value)} className="w-full text-xs px-2 py-1.5 border border-gray-200 rounded font-mono" /> <button onClick={() => handleRemoveColor(index)} disabled={colors.length === 1} className={`p-1.5 rounded ${ colors.length === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-red-500 hover:bg-red-50' }`} > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12"/> </svg> </button> </div> ))} </div> {/* Add Color */} {colors.length < 10 && ( <div className="flex items-center gap-2"> <input type="color" value={newColor} onChange={(e) => setNewColor(e.target.value)} className="w-8 aspect-square rounded cursor-pointer border border-gray-200" /> <input type="text" value={newColor} onChange={(e) => setNewColor(e.target.value)} className="w-full text-xs px-2 py-1.5 border border-gray-200 rounded font-mono" placeholder="#ff6b6b" /> <button onClick={handleAddColor} className="p-1.5 rounded text-emerald-500 hover:bg-emerald-50" > <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4"/> </svg> </button> </div> )} <p className="text-[0.65rem] text-gray-400 mt-2 leading-tight"> {colors.length < 10 ? `Add up to ${10 - colors.length} more color(s)` : 'Maximum colors reached'} </p> </div> </div> <style jsx>{` .slider::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:12px; height:12px; border-radius:50%; background:#10b981; cursor:pointer; box-shadow:0 1px 3px rgba(0, 0, 0, 0.15); } .slider::-moz-range-thumb { width:12px; height:12px; border-radius:50%; background:#10b981; cursor:pointer; border:none; box-shadow:0 1px 3px rgba(0, 0, 0, 0.15); } /* Scrollbar */ .custom-scrollbar::-webkit-scrollbar { width:5px; height:8px; /* Height for horizontal scrollbar */ } .custom-scrollbar::-webkit-scrollbar-track { background:#f1f5f9; /* Light gray track */ border-radius:4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background:#10b981; /* Emerald green thumb */ border-radius:4px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background:#059669; /* Darker emerald on hover */ } /* Firefox */ .custom-scrollbar { scrollbar-width:thin; scrollbar-color:#10b981 #f1f5f9; } `}</style> </div> </div> ); }
Last updated on