Github Gradient Button
A gradient border with rounded corners applied to any div or text, enhancing content with a colorful, stylish edge
A gradient border with rounded corners applied to any div or text, enhancing content with a colorful, stylish edge
Thanks To Oni
npm install framer-motion
1'use client';2import { motion, AnimatePresence } from 'framer-motion';3type ColorKey =4| 'color1'5| 'color2'6| 'color3'7| 'color4'8| 'color5'9| 'color6'10| 'color7'11| 'color8'12| 'color9'13| 'color10'14| 'color11'15| 'color12'16| 'color13'17| 'color14'18| 'color15'19| 'color16'20| 'color17';2122export type Colors = Record<ColorKey, string>;2324const svgOrder = [25'svg1',26'svg2',27'svg3',28'svg4',29'svg3',30'svg2',31'svg1',32] as const;3334type SvgKey = (typeof svgOrder)[number];3536type Stop = {37offset: number;38stopColor: string;39};4041type SvgState = {42gradientTransform: string;43stops: Stop[];44};4546type SvgStates = Record<SvgKey, SvgState>;4748const createStopsArray = (49svgStates: SvgStates,50svgOrder: readonly SvgKey[],51maxStops: number52): Stop[][] => {53let stopsArray: Stop[][] = [];54for (let i = 0; i < maxStops; i++) {55let stopConfigurations = svgOrder.map((svgKey) => {56let svg = svgStates[svgKey];57return svg.stops[i] || svg.stops[svg.stops.length - 1];58});59stopsArray.push(stopConfigurations);60}61return stopsArray;62};6364type GradientSvgProps = {65className: string;66isHovered: boolean;67colors: Colors;68};6970const GradientSvg: React.FC<GradientSvgProps> = ({71className,72isHovered,73colors,74}) => {75const svgStates: SvgStates = {76svg1: {77gradientTransform:78'translate(287.5 280) rotate(-29.0546) scale(689.807 1000)',79stops: [80{ offset: 0, stopColor: colors.color1 },81{ offset: 0.188423, stopColor: colors.color2 },82{ offset: 0.260417, stopColor: colors.color3 },83{ offset: 0.328792, stopColor: colors.color4 },84{ offset: 0.328892, stopColor: colors.color5 },85{ offset: 0.328992, stopColor: colors.color1 },86{ offset: 0.442708, stopColor: colors.color6 },87{ offset: 0.537556, stopColor: colors.color7 },88{ offset: 0.631738, stopColor: colors.color1 },89{ offset: 0.725645, stopColor: colors.color8 },90{ offset: 0.817779, stopColor: colors.color9 },91{ offset: 0.84375, stopColor: colors.color10 },92{ offset: 0.90569, stopColor: colors.color1 },93{ offset: 1, stopColor: colors.color11 },94],95},96svg2: {97gradientTransform:98'translate(126.5 418.5) rotate(-64.756) scale(533.444 773.324)',99stops: [100{ offset: 0, stopColor: colors.color1 },101{ offset: 0.104167, stopColor: colors.color12 },102{ offset: 0.182292, stopColor: colors.color13 },103{ offset: 0.28125, stopColor: colors.color1 },104{ offset: 0.328792, stopColor: colors.color4 },105{ offset: 0.328892, stopColor: colors.color5 },106{ offset: 0.453125, stopColor: colors.color6 },107{ offset: 0.515625, stopColor: colors.color7 },108{ offset: 0.631738, stopColor: colors.color1 },109{ offset: 0.692708, stopColor: colors.color8 },110{ offset: 0.75, stopColor: colors.color14 },111{ offset: 0.817708, stopColor: colors.color9 },112{ offset: 0.869792, stopColor: colors.color10 },113{ offset: 1, stopColor: colors.color1 },114],115},116svg3: {117gradientTransform:118'translate(264.5 339.5) rotate(-42.3022) scale(946.451 1372.05)',119stops: [120{ offset: 0, stopColor: colors.color1 },121{ offset: 0.188423, stopColor: colors.color2 },122{ offset: 0.307292, stopColor: colors.color1 },123{ offset: 0.328792, stopColor: colors.color4 },124{ offset: 0.328892, stopColor: colors.color5 },125{ offset: 0.442708, stopColor: colors.color15 },126{ offset: 0.537556, stopColor: colors.color16 },127{ offset: 0.631738, stopColor: colors.color1 },128{ offset: 0.725645, stopColor: colors.color17 },129{ offset: 0.817779, stopColor: colors.color9 },130{ offset: 0.84375, stopColor: colors.color10 },131{ offset: 0.90569, stopColor: colors.color1 },132{ offset: 1, stopColor: colors.color11 },133],134},135svg4: {136gradientTransform:137'translate(860.5 420) rotate(-153.984) scale(957.528 1388.11)',138stops: [139{ offset: 0.109375, stopColor: colors.color11 },140{ offset: 0.171875, stopColor: colors.color2 },141{ offset: 0.260417, stopColor: colors.color13 },142{ offset: 0.328792, stopColor: colors.color4 },143{ offset: 0.328892, stopColor: colors.color5 },144{ offset: 0.328992, stopColor: colors.color1 },145{ offset: 0.442708, stopColor: colors.color6 },146{ offset: 0.515625, stopColor: colors.color7 },147{ offset: 0.631738, stopColor: colors.color1 },148{ offset: 0.692708, stopColor: colors.color8 },149{ offset: 0.817708, stopColor: colors.color9 },150{ offset: 0.869792, stopColor: colors.color10 },151{ offset: 1, stopColor: colors.color11 },152],153},154};155156const maxStops = Math.max(157...Object.values(svgStates).map((svg) => svg.stops.length)158);159const stopsAnimationArray = createStopsArray(svgStates, svgOrder, maxStops);160const gradientTransform = svgOrder.map(161(svgKey) => svgStates[svgKey].gradientTransform162);163164const variants = {165hovered: {166gradientTransform: gradientTransform,167transition: { duration: 50, repeat: Infinity, ease: 'linear' },168},169notHovered: {170gradientTransform: gradientTransform,171transition: { duration: 10, repeat: Infinity, ease: 'linear' },172},173};174175return (176<svg177className={className}178width='1030'179height='280'180viewBox='0 0 1030 280'181fill='none'182xmlns='http://www.w3.org/2000/svg'183>184<rect185width='1030'186height='280'187rx='140'188fill='url(#paint0_radial_905_231)'189/>190<defs>191<motion.radialGradient192id='paint0_radial_905_231'193cx='0'194cy='0'195r='1'196gradientUnits='userSpaceOnUse'197animate={isHovered ? variants.hovered : variants.notHovered}198>199{stopsAnimationArray.map((stopConfigs, index) => (200<AnimatePresence key={index}>201<motion.stop202initial={{203offset: stopConfigs[0].offset,204stopColor: stopConfigs[0].stopColor,205}}206animate={{207offset: stopConfigs.map((config) => config.offset),208stopColor: stopConfigs.map((config) => config.stopColor),209}}210transition={{211duration: 0,212ease: 'linear',213repeat: Infinity,214}}215/>216</AnimatePresence>217))}218</motion.radialGradient>219</defs>220</svg>221);222};223224type LiquidProps = {225isHovered: boolean;226colors: Colors;227};228229export const Liquid: React.FC<LiquidProps> = ({ isHovered, colors }) => {230return (231<>232{Array.from({ length: 7 }).map((_, index) => (233<div234key={index}235className={`absolute ${236index < 3 ? 'w-[443px] h-[121px]' : 'w-[756px] h-[207px]'237} ${238index === 0239? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 mix-blend-difference'240: index === 1241? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-[164.971deg] mix-blend-difference'242: index === 2243? 'top-1/2 left-1/2 -translate-x-[53%] -translate-y-[53%] rotate-[-11.61deg] mix-blend-difference'244: index === 3245? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-[57%] rotate-[-179.012deg] mix-blend-difference'246: index === 4247? 'top-1/2 left-1/2 -translate-x-[57%] -translate-y-1/2 rotate-[-29.722deg] mix-blend-difference'248: index === 5249? 'top-1/2 left-1/2 -translate-x-[62%] -translate-y-[24%] rotate-[160.227deg] mix-blend-difference'250: 'top-1/2 left-1/2 -translate-x-[67%] -translate-y-[29%] rotate-180 mix-blend-hard-light'251}`}252>253<GradientSvg254className='w-full h-full'255isHovered={isHovered}256colors={colors}257/>258</div>259))}260</>261);262};
Name | Type | Required | Description |
---|---|---|---|
isHovered | boolean | Yes | Controls the animation state (hovered or not). |
colors | Colors | Yes | A record of color keys and their corresponding color values. |