20 CSS Gradient Text Designs 16 / 20
CSS Click Ripple Gradient Text Interaction
Clicking the gradient headline spawns a ripple ring at the click point while the headline flashes brighter, demonstrating CSS keyframe injection via JavaScript.
The code
<div class="gt-16">
<span class="gt-16__label">Click / tap gradient text</span>
<span class="gt-16__hint">↓ click the word ↓</span>
<div class="gt-16__btn" id="gt-16-main">
CLICK
<div class="gt-16__ripples" id="gt-16-ripples"></div>
</div>
<div class="gt-16__score">Clicks: <span id="gt-16-count">0</span></div>
<div class="gt-16__palette" id="gt-16-palette">
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#ff6b6b,#ffd93d,#6bcb77,#4d96ff,#a855f7,#ff6b6b)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#6bcb77,#4d96ff,#a855f7,#ec4899,#6bcb77)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#a855f7,#ec4899,#f43f5e,#fb923c,#a855f7)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#00f5a0,#00d9f5,#4d96ff,#a855f7,#00f5a0)"></div>
</div>
</div>
<script>
(function() {
const btn = document.getElementById('gt-16-main');
const ripples = document.getElementById('gt-16-ripples');
const countEl = document.getElementById('gt-16-count');
const palette = document.getElementById('gt-16-palette');
let count = 0;
const colors = ['#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#a855f7','#ec4899','#00f5a0'];
btn.addEventListener('click', (e) => {
count++;
countEl.textContent = count;
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const r = document.createElement('div');
r.className = 'gt-16__ripple';
const c = colors[Math.floor(Math.random() * colors.length)];
r.style.cssText = `left:${x}px;top:${y}px;width:80px;height:80px;background:${c};`;
ripples.appendChild(r);
btn.classList.remove('is-burst');
void btn.offsetWidth;
btn.classList.add('is-burst');
r.addEventListener('animationend', () => r.remove());
});
palette.querySelectorAll('.gt-16__swatch').forEach(sw => {
sw.addEventListener('click', () => {
btn.style.backgroundImage = sw.dataset.grad;
});
});
})();
</script> <div class="gt-16">
<span class="gt-16__label">Click / tap gradient text</span>
<span class="gt-16__hint">↓ click the word ↓</span>
<div class="gt-16__btn" id="gt-16-main">
CLICK
<div class="gt-16__ripples" id="gt-16-ripples"></div>
</div>
<div class="gt-16__score">Clicks: <span id="gt-16-count">0</span></div>
<div class="gt-16__palette" id="gt-16-palette">
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#ff6b6b,#ffd93d,#6bcb77,#4d96ff,#a855f7,#ff6b6b)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#6bcb77,#4d96ff,#a855f7,#ec4899,#6bcb77)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#a855f7,#ec4899,#f43f5e,#fb923c,#a855f7)"></div>
<div class="gt-16__swatch" data-grad="linear-gradient(90deg,#00f5a0,#00d9f5,#4d96ff,#a855f7,#00f5a0)"></div>
</div>
</div>
<script>
(function() {
const btn = document.getElementById('gt-16-main');
const ripples = document.getElementById('gt-16-ripples');
const countEl = document.getElementById('gt-16-count');
const palette = document.getElementById('gt-16-palette');
let count = 0;
const colors = ['#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#a855f7','#ec4899','#00f5a0'];
btn.addEventListener('click', (e) => {
count++;
countEl.textContent = count;
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const r = document.createElement('div');
r.className = 'gt-16__ripple';
const c = colors[Math.floor(Math.random() * colors.length)];
r.style.cssText = `left:${x}px;top:${y}px;width:80px;height:80px;background:${c};`;
ripples.appendChild(r);
btn.classList.remove('is-burst');
void btn.offsetWidth;
btn.classList.add('is-burst');
r.addEventListener('animationend', () => r.remove());
});
palette.querySelectorAll('.gt-16__swatch').forEach(sw => {
sw.addEventListener('click', () => {
btn.style.backgroundImage = sw.dataset.grad;
});
});
})();
</script>.gt-16, .gt-16 *, .gt-16 *::before, .gt-16 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.gt-16 {
--bg: #f8f8f4;
font-family: 'Outfit', sans-serif;
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 3rem 2rem;
user-select: none;
}
.gt-16__label {
font-size: .7rem;
letter-spacing: .2em;
text-transform: uppercase;
color: #ddd;
}
.gt-16__hint {
font-size: .75rem;
color: #ccc;
letter-spacing: .1em;
}
.gt-16__btn {
font-size: clamp(3rem, 12vw, 8rem);
font-weight: 900;
line-height: 1;
letter-spacing: .02em;
background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #a855f7, #ff6b6b);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
cursor: pointer;
position: relative;
transition: transform .15s ease;
background-position: 0% center;
animation: gt-16-drift 8s linear infinite;
}
.gt-16__btn:active {
transform: scale(.97);
}
.gt-16__btn.is-burst {
animation: gt-16-burst .5s ease forwards;
}
.gt-16__ripples {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
}
.gt-16__ripple {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
animation: gt-16-ripple .8s ease-out forwards;
pointer-events: none;
opacity: .5;
}
.gt-16__score {
font-size: 1rem;
font-weight: 800;
color: #ddd;
letter-spacing: .1em;
}
.gt-16__score span {
background: linear-gradient(90deg, #ff6b6b, #a855f7);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.gt-16__palette {
display: flex;
gap: .5rem;
}
.gt-16__swatch {
width: 20px; height: 20px;
border-radius: 50%;
cursor: pointer;
transition: transform .2s;
border: 2px solid white;
}
.gt-16__swatch:hover { transform: scale(1.2); }
.gt-16__swatch:nth-child(1) { background: linear-gradient(135deg, #ff6b6b, #ffd93d); }
.gt-16__swatch:nth-child(2) { background: linear-gradient(135deg, #6bcb77, #4d96ff); }
.gt-16__swatch:nth-child(3) { background: linear-gradient(135deg, #a855f7, #ec4899); }
.gt-16__swatch:nth-child(4) { background: linear-gradient(135deg, #00f5a0, #00d9f5); }
@keyframes gt-16-drift {
0% { background-position: 0% center; }
100% { background-position: 300% center; }
}
@keyframes gt-16-burst {
0% { transform: scale(1); filter: brightness(1); }
30% { transform: scale(1.06); filter: brightness(1.3); }
100% { transform: scale(1); filter: brightness(1); }
}
@keyframes gt-16-ripple {
0% { transform: translate(-50%, -50%) scale(0); opacity: .6; }
100% { transform: translate(-50%, -50%) scale(6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.gt-16__btn { animation: none; }
.gt-16__btn.is-burst { animation: none; }
.gt-16__ripple { animation: none; display: none; }
} .gt-16, .gt-16 *, .gt-16 *::before, .gt-16 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.gt-16 {
--bg: #f8f8f4;
font-family: 'Outfit', sans-serif;
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 3rem 2rem;
user-select: none;
}
.gt-16__label {
font-size: .7rem;
letter-spacing: .2em;
text-transform: uppercase;
color: #ddd;
}
.gt-16__hint {
font-size: .75rem;
color: #ccc;
letter-spacing: .1em;
}
.gt-16__btn {
font-size: clamp(3rem, 12vw, 8rem);
font-weight: 900;
line-height: 1;
letter-spacing: .02em;
background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #a855f7, #ff6b6b);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
cursor: pointer;
position: relative;
transition: transform .15s ease;
background-position: 0% center;
animation: gt-16-drift 8s linear infinite;
}
.gt-16__btn:active {
transform: scale(.97);
}
.gt-16__btn.is-burst {
animation: gt-16-burst .5s ease forwards;
}
.gt-16__ripples {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
}
.gt-16__ripple {
position: absolute;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
animation: gt-16-ripple .8s ease-out forwards;
pointer-events: none;
opacity: .5;
}
.gt-16__score {
font-size: 1rem;
font-weight: 800;
color: #ddd;
letter-spacing: .1em;
}
.gt-16__score span {
background: linear-gradient(90deg, #ff6b6b, #a855f7);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.gt-16__palette {
display: flex;
gap: .5rem;
}
.gt-16__swatch {
width: 20px; height: 20px;
border-radius: 50%;
cursor: pointer;
transition: transform .2s;
border: 2px solid white;
}
.gt-16__swatch:hover { transform: scale(1.2); }
.gt-16__swatch:nth-child(1) { background: linear-gradient(135deg, #ff6b6b, #ffd93d); }
.gt-16__swatch:nth-child(2) { background: linear-gradient(135deg, #6bcb77, #4d96ff); }
.gt-16__swatch:nth-child(3) { background: linear-gradient(135deg, #a855f7, #ec4899); }
.gt-16__swatch:nth-child(4) { background: linear-gradient(135deg, #00f5a0, #00d9f5); }
@keyframes gt-16-drift {
0% { background-position: 0% center; }
100% { background-position: 300% center; }
}
@keyframes gt-16-burst {
0% { transform: scale(1); filter: brightness(1); }
30% { transform: scale(1.06); filter: brightness(1.3); }
100% { transform: scale(1); filter: brightness(1); }
}
@keyframes gt-16-ripple {
0% { transform: translate(-50%, -50%) scale(0); opacity: .6; }
100% { transform: translate(-50%, -50%) scale(6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.gt-16__btn { animation: none; }
.gt-16__btn.is-burst { animation: none; }
.gt-16__ripple { animation: none; display: none; }
}(function() {
const btn = document.getElementById('gt-16-main');
const ripples = document.getElementById('gt-16-ripples');
const countEl = document.getElementById('gt-16-count');
const palette = document.getElementById('gt-16-palette');
let count = 0;
const colors = ['#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#a855f7','#ec4899','#00f5a0'];
btn.addEventListener('click', (e) => {
count++;
countEl.textContent = count;
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const r = document.createElement('div');
r.className = 'gt-16__ripple';
const c = colors[Math.floor(Math.random() * colors.length)];
r.style.cssText = `left:${x}px;top:${y}px;width:80px;height:80px;background:${c};`;
ripples.appendChild(r);
btn.classList.remove('is-burst');
void btn.offsetWidth;
btn.classList.add('is-burst');
r.addEventListener('animationend', () => r.remove());
});
palette.querySelectorAll('.gt-16__swatch').forEach(sw => {
sw.addEventListener('click', () => {
btn.style.backgroundImage = sw.dataset.grad;
});
});
})(); (function() {
const btn = document.getElementById('gt-16-main');
const ripples = document.getElementById('gt-16-ripples');
const countEl = document.getElementById('gt-16-count');
const palette = document.getElementById('gt-16-palette');
let count = 0;
const colors = ['#ff6b6b','#ffd93d','#6bcb77','#4d96ff','#a855f7','#ec4899','#00f5a0'];
btn.addEventListener('click', (e) => {
count++;
countEl.textContent = count;
const rect = btn.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const r = document.createElement('div');
r.className = 'gt-16__ripple';
const c = colors[Math.floor(Math.random() * colors.length)];
r.style.cssText = `left:${x}px;top:${y}px;width:80px;height:80px;background:${c};`;
ripples.appendChild(r);
btn.classList.remove('is-burst');
void btn.offsetWidth;
btn.classList.add('is-burst');
r.addEventListener('animationend', () => r.remove());
});
palette.querySelectorAll('.gt-16__swatch').forEach(sw => {
sw.addEventListener('click', () => {
btn.style.backgroundImage = sw.dataset.grad;
});
});
})();How this works
The headline continuously scrolls its gradient via a CSS animation. On click, JavaScript reads the pointer coordinates relative to the element bounding rect and creates a div.gt-16__ripple positioned at that exact point. The ripple has a fixed width and height and uses transform: translate(-50%, -50%) scale(0) as its start state, scaling out to scale(6) with opacity: 0 via the gt-16-ripple keyframe.
A separate .is-burst class triggers the gt-16-burst keyframe that briefly scales and brightens the headline. The class is removed and re-added via classList.remove / offsetWidth / classList.add (the DOM reflow trick) so the animation restarts on every click.
Customize
- Increase ripple count by not removing
gt-16__rippleelements after their animation ends — up to a point, overlapping rings add visual richness without memory issues. - Change ripple start size by editing the
width/height: 80pxon.gt-16__ripple— a smaller start size gives a sharper, more energetic burst. - Pass the click coordinates as CSS custom properties (
--rx,--ry) to drive a radial-gradient spotlight effect on the headline itself during the burst.
Watch out for
- The
classList.remove / offsetWidth / classList.addpattern to restart CSS animations forces a synchronous layout reflow — avoid using it more than a few times per second. - Ripple elements appended to the DOM accumulate if
animationendevents are not fired (e.g. when the tab is backgrounded) — always add a timeout fallback to remove orphaned ripples. - Pointer coordinates derived from
e.clientXandgetBoundingClientRectwill be wrong if the element is inside a CSStransformparent — usee.offsetX/Yinstead.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 58+ | 12.1+ | 55+ | 58+ |
animationend event is well-supported; getBoundingClientRect inside a CSS transform parent may give unexpected coordinates in Safari 14 and below.