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.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

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>
.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;
    });
  });
})();

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__ripple elements 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: 80px on .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.add pattern 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 animationend events are not fired (e.g. when the tab is backgrounded) — always add a timeout fallback to remove orphaned ripples.
  • Pointer coordinates derived from e.clientX and getBoundingClientRect will be wrong if the element is inside a CSS transform parent — use e.offsetX/Y instead.

Browser support

ChromeSafariFirefoxEdge
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.

Search CodeFronts

Loading…