Stackbits
Text > Gooey Words

Gooey Words

The Gooey Words component adds a delightful splash of motion and creativity to your UI. Watch as vibrant, animated text words like "GLIDE", "SHINE", and "CLOUD" morph and bounce in a mesmerizing gooey effect using Framer Motion. Perfect for hero sections, splash pages, or just to add some playful flair to your design. It's responsive, interactive, and ridiculously fun — because text should never be boring. Bring your interface to life with bouncy vibes and smooth animations

Preview

Follow below steps 👇🏻

Install dependencies

1
npm i framer-motion

Component

Create a file gooey-words.tsx in your components folder and paste this code

1
'use client';
2
3
import { animate, AnimatePresence, motion } from 'framer-motion';
4
import React, { memo, RefObject, useEffect, useRef, useState } from 'react';
5
6
const alphabetPaths = {
7
A: 'M15,90 L50,10 L85,90 M25,60 H75',
8
B: 'M20,10 V90 H60 Q85,90 85,70 Q85,60 70,50 Q85,40 85,30 Q85,10 60,10 H20',
9
C: 'M85,25 Q65,10 40,10 Q15,10 15,50 Q15,90 40,90 Q65,90 85,75',
10
D: 'M20,10 V90 H60 Q90,90 90,50 Q90,10 60,10 H20',
11
E: 'M80,10 H20 V90 H80 M20,50 H65',
12
F: 'M20,10 V90 M20,10 H80 M20,50 H65',
13
G: 'M85,25 Q65,10 40,10 Q15,10 15,50 Q15,90 40,90 Q65,90 85,75 V55 H55',
14
H: 'M20,10 V90 M80,10 V90 M20,50 H80',
15
I: 'M30,10 H70 M50,10 V90 M30,90 H70',
16
J: 'M70,10 H30 M50,10 V75 Q50,90 30,85',
17
K: 'M20,10 V90 M80,10 L20,50 L80,90',
18
L: 'M20,10 V90 H80',
19
M: 'M15,90 V15 L50,50 L85,15 V90',
20
N: 'M20,90 V15 L80,90 V15',
21
O: 'M50,10 Q80,10 85,50 Q80,90 50,90 Q20,90 15,50 Q20,10 50,10',
22
P: 'M20,90 V10 H65 Q85,10 85,35 Q85,60 65,60 H20',
23
Q: 'M50,10 Q80,10 85,50 Q80,90 50,90 Q20,90 15,50 Q20,10 50,10 M60,70 L85,90',
24
R: 'M20,90 V10 H65 Q85,10 85,35 Q85,60 65,60 H20 M50,60 L85,90',
25
S: 'M80,25 Q60,10 35,10 Q15,15 15,35 Q15,50 50,55 Q85,60 85,75 Q85,90 50,90 Q25,90 15,75',
26
T: 'M15,10 H85 M50,10 V90',
27
U: 'M15,10 V65 Q15,90 50,90 Q85,90 85,65 V10',
28
V: 'M15,10 L50,90 L85,10',
29
W: 'M10,10 L30,90 L50,40 L70,90 L90,10',
30
X: 'M15,10 L85,90 M15,90 L85,10',
31
Y: 'M15,10 L50,50 L85,10 M50,50 V90',
32
Z: 'M15,10 H85 L15,90 H85'
33
};
34
35
const Letter = memo(
36
({
37
letterIndex,
38
wordPathsRef,
39
index,
40
path,
41
circles,
42
circlesRef,
43
circlesPositionsRef,
44
radius
45
}: {
46
letterIndex: number;
47
index: number;
48
path: string;
49
radius: number;
50
wordPathsRef: RefObject<Record<number, SVGPathElement[]>>;
51
circles: number;
52
circlesRef: RefObject<Record<number, SVGCircleElement[][]>>;
53
circlesPositionsRef: RefObject<{ x: number; y: number }[][]>;
54
}) => {
55
return (
56
<div className="mx-1">
57
<svg width="50" height="70" viewBox="0 0 100 100" style={{ filter: 'url(#gooey)' }}>
58
<path
59
ref={(el) => {
60
if (el) {
61
if (!wordPathsRef.current[index]) {
62
wordPathsRef.current[index] = [];
63
}
64
wordPathsRef.current[index][letterIndex] = el;
65
}
66
}}
67
d={path}
68
fill="none"
69
strokeLinecap="round"
70
strokeLinejoin="round"
71
/>
72
<g>
73
{Array.from({ length: circles }).map((_, circleIndex) => (
74
<circle
75
ref={(el) => {
76
if (el) {
77
if (!circlesRef.current[index]) {
78
circlesRef.current[index] = [];
79
}
80
if (!circlesRef.current[index][letterIndex]) {
81
circlesRef.current[index][letterIndex] = [];
82
}
83
if (!circlesPositionsRef.current[letterIndex]) {
84
circlesPositionsRef.current[letterIndex] = [];
85
}
86
if (!circlesPositionsRef.current[letterIndex][circleIndex]) {
87
circlesPositionsRef.current[letterIndex][circleIndex] = {
88
x: 50,
89
y: 50
90
};
91
}
92
93
circlesRef.current[index][letterIndex][circleIndex] = el;
94
}
95
}}
96
r={radius}
97
key={`circle-${letterIndex}-${circleIndex}-${index}`}
98
cx={circlesPositionsRef.current?.[letterIndex]?.[circleIndex].x || 50}
99
cy={circlesPositionsRef.current?.[letterIndex]?.[circleIndex].y || 50}
100
fill="white"
101
/>
102
))}
103
</g>
104
</svg>
105
</div>
106
);
107
}
108
);
109
110
const GooeyWords = ({ words, speed = 2 }: { words: string[]; speed?: number }) => {
111
const [index, setIndex] = useState(0);
112
const wordPathsRef = useRef<Record<number, SVGPathElement[]>>({});
113
const circlesRef = useRef<Record<number, SVGCircleElement[][]>>({});
114
const circlesPositionsRef = useRef<{ x: number; y: number }[][]>([]);
115
116
const circles = 25;
117
const radius = 5;
118
119
useEffect(() => {
120
const interval = setInterval(() => {
121
setIndex((i) => (i + 1) % words.length);
122
}, speed * 1000);
123
124
return () => clearInterval(interval);
125
}, [words.length, speed, index]);
126
127
useEffect(() => {
128
if (!wordPathsRef.current[index] || wordPathsRef.current[index].length === 0) {
129
return;
130
}
131
132
const currentPaths = wordPathsRef.current[index];
133
const currentCirclesGroups = circlesRef.current[index] || [];
134
135
currentPaths.forEach((path, letterIndex) => {
136
if (!path) return;
137
138
const length = path.getTotalLength();
139
if (!length) return;
140
141
const step = length / circles;
142
const circlesToAnimate = currentCirclesGroups[letterIndex] || [];
143
144
circlesToAnimate.forEach((circle, circleIndex) => {
145
if (!circle) return;
146
147
const { x, y } = path.getPointAtLength(step * circleIndex) || { x: 0, y: 0 };
148
149
circlesPositionsRef.current[letterIndex][circleIndex] = { x, y };
150
151
animate(
152
circle,
153
{ cx: x, cy: y },
154
{
155
duration: 0.8,
156
delay: 0.02 * circleIndex + letterIndex * 0.04,
157
ease: 'easeOut'
158
}
159
);
160
});
161
});
162
}, [index, circles]);
163
164
return (
165
<div className="relative">
166
<svg className="absolute w-0 h-0">
167
<defs>
168
<filter id="gooey">
169
<feGaussianBlur in="SourceGraphic" stdDeviation="2" result="blur" />
170
<feColorMatrix
171
in="blur"
172
type="matrix"
173
values="
174
1 0 0 0 0
175
0 1 0 0 0
176
0 0 1 0 0
177
0 0 0 12 -2"
178
result="gooey"
179
/>
180
</filter>
181
</defs>
182
</svg>
183
184
<AnimatePresence mode="popLayout">
185
<motion.div
186
className="flex items-center justify-center p-8 rounded-lg"
187
style={{
188
minHeight: '100px'
189
}}
190
>
191
{words[index].split('').map((letter, letterIndex) => {
192
const path = alphabetPaths[letter.toUpperCase() as keyof typeof alphabetPaths];
193
194
if (!path) {
195
return <div key={`${letter}-${letterIndex}-${index}`} className="w-8" />;
196
}
197
198
return (
199
<Letter
200
key={`${letter}-${letterIndex}-${index}`}
201
letterIndex={letterIndex}
202
index={index}
203
path={path}
204
radius={radius}
205
wordPathsRef={wordPathsRef}
206
circles={circles}
207
circlesRef={circlesRef}
208
circlesPositionsRef={circlesPositionsRef}
209
/>
210
);
211
})}
212
</motion.div>
213
</AnimatePresence>
214
</div>
215
);
216
};
217
218
export default GooeyWords;

Usage

1
<GooeyWords words={words} />

⭐️ Got a question or feedback?
Feel free to reach out!