Github Gradient Button

A gradient border with rounded corners applied to any div or text, enhancing content with a colorful, stylish edge

Thanks To Oni

Installation

npm install framer-motion
liquid-gradient.tsx
1
'use client';
2
import { motion, AnimatePresence } from 'framer-motion';
3
type 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';
21
22
export type Colors = Record<ColorKey, string>;
23
24
const svgOrder = [
25
'svg1',
26
'svg2',
27
'svg3',
28
'svg4',
29
'svg3',
30
'svg2',
31
'svg1',
32
] as const;
33
34
type SvgKey = (typeof svgOrder)[number];
35
36
type Stop = {
37
offset: number;
38
stopColor: string;
39
};
40
41
type SvgState = {
42
gradientTransform: string;
43
stops: Stop[];
44
};
45
46
type SvgStates = Record<SvgKey, SvgState>;
47
48
const createStopsArray = (
49
svgStates: SvgStates,
50
svgOrder: readonly SvgKey[],
51
maxStops: number
52
): Stop[][] => {
53
let stopsArray: Stop[][] = [];
54
for (let i = 0; i < maxStops; i++) {
55
let stopConfigurations = svgOrder.map((svgKey) => {
56
let svg = svgStates[svgKey];
57
return svg.stops[i] || svg.stops[svg.stops.length - 1];
58
});
59
stopsArray.push(stopConfigurations);
60
}
61
return stopsArray;
62
};
63
64
type GradientSvgProps = {
65
className: string;
66
isHovered: boolean;
67
colors: Colors;
68
};
69
70
const GradientSvg: React.FC<GradientSvgProps> = ({
71
className,
72
isHovered,
73
colors,
74
}) => {
75
const svgStates: SvgStates = {
76
svg1: {
77
gradientTransform:
78
'translate(287.5 280) rotate(-29.0546) scale(689.807 1000)',
79
stops: [
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
},
96
svg2: {
97
gradientTransform:
98
'translate(126.5 418.5) rotate(-64.756) scale(533.444 773.324)',
99
stops: [
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
},
116
svg3: {
117
gradientTransform:
118
'translate(264.5 339.5) rotate(-42.3022) scale(946.451 1372.05)',
119
stops: [
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
},
135
svg4: {
136
gradientTransform:
137
'translate(860.5 420) rotate(-153.984) scale(957.528 1388.11)',
138
stops: [
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
};
155
156
const maxStops = Math.max(
157
...Object.values(svgStates).map((svg) => svg.stops.length)
158
);
159
const stopsAnimationArray = createStopsArray(svgStates, svgOrder, maxStops);
160
const gradientTransform = svgOrder.map(
161
(svgKey) => svgStates[svgKey].gradientTransform
162
);
163
164
const variants = {
165
hovered: {
166
gradientTransform: gradientTransform,
167
transition: { duration: 50, repeat: Infinity, ease: 'linear' },
168
},
169
notHovered: {
170
gradientTransform: gradientTransform,
171
transition: { duration: 10, repeat: Infinity, ease: 'linear' },
172
},
173
};
174
175
return (
176
<svg
177
className={className}
178
width='1030'
179
height='280'
180
viewBox='0 0 1030 280'
181
fill='none'
182
xmlns='http://www.w3.org/2000/svg'
183
>
184
<rect
185
width='1030'
186
height='280'
187
rx='140'
188
fill='url(#paint0_radial_905_231)'
189
/>
190
<defs>
191
<motion.radialGradient
192
id='paint0_radial_905_231'
193
cx='0'
194
cy='0'
195
r='1'
196
gradientUnits='userSpaceOnUse'
197
animate={isHovered ? variants.hovered : variants.notHovered}
198
>
199
{stopsAnimationArray.map((stopConfigs, index) => (
200
<AnimatePresence key={index}>
201
<motion.stop
202
initial={{
203
offset: stopConfigs[0].offset,
204
stopColor: stopConfigs[0].stopColor,
205
}}
206
animate={{
207
offset: stopConfigs.map((config) => config.offset),
208
stopColor: stopConfigs.map((config) => config.stopColor),
209
}}
210
transition={{
211
duration: 0,
212
ease: 'linear',
213
repeat: Infinity,
214
}}
215
/>
216
</AnimatePresence>
217
))}
218
</motion.radialGradient>
219
</defs>
220
</svg>
221
);
222
};
223
224
type LiquidProps = {
225
isHovered: boolean;
226
colors: Colors;
227
};
228
229
export const Liquid: React.FC<LiquidProps> = ({ isHovered, colors }) => {
230
return (
231
<>
232
{Array.from({ length: 7 }).map((_, index) => (
233
<div
234
key={index}
235
className={`absolute ${
236
index < 3 ? 'w-[443px] h-[121px]' : 'w-[756px] h-[207px]'
237
} ${
238
index === 0
239
? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 mix-blend-difference'
240
: index === 1
241
? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-[164.971deg] mix-blend-difference'
242
: index === 2
243
? 'top-1/2 left-1/2 -translate-x-[53%] -translate-y-[53%] rotate-[-11.61deg] mix-blend-difference'
244
: index === 3
245
? 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-[57%] rotate-[-179.012deg] mix-blend-difference'
246
: index === 4
247
? 'top-1/2 left-1/2 -translate-x-[57%] -translate-y-1/2 rotate-[-29.722deg] mix-blend-difference'
248
: index === 5
249
? '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
<GradientSvg
254
className='w-full h-full'
255
isHovered={isHovered}
256
colors={colors}
257
/>
258
</div>
259
))}
260
</>
261
);
262
};

Props

NameTypeRequiredDescription
isHoveredbooleanYesControls the animation state (hovered or not).
colorsColorsYesA record of color keys and their corresponding color values.