20 CSS Gradient Text Designs 15 / 20

CSS Scroll Gradient Text Reveal on Scroll

Each section's headline fades in from below with a gradient fill sliding from washed-out to vivid, triggered by IntersectionObserver as the element enters the viewport.

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

The code

<div class="gt-15">
  <div class="gt-15__top">
    <p class="gt-15__hint">Scroll to reveal gradient text</p>
    <span class="gt-15__arrow">↓</span>
  </div>
  <div class="gt-15__section">
    <div class="gt-15__reveal">DISCOVER</div>
  </div>
  <div class="gt-15__section">
    <div class="gt-15__reveal">YOUR</div>
  </div>
  <div class="gt-15__section">
    <div class="gt-15__reveal">POTENTIAL</div>
  </div>
  <div class="gt-15__section">
    <div class="gt-15__reveal">THROUGH COLOUR</div>
  </div>
</div>
<script>
(function() {
  const items = document.querySelectorAll('.gt-15 .gt-15__reveal');
  if (!items.length) return;
  const io = new IntersectionObserver((entries) => {
    entries.forEach(e => {
      if (e.isIntersecting) {
        e.target.classList.add('is-visible');
      }
    });
  }, { threshold: 0.3 });
  items.forEach(el => io.observe(el));
})();
</script>
.gt-15, .gt-15 *, .gt-15 *::before, .gt-15 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.gt-15 {
  --bg: #0c0c0c;
  font-family: 'Syne', sans-serif;
  background: var(--bg);
  min-height: 300vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 2rem;
}
.gt-15__top {
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}
.gt-15__hint {
  font-size: .8rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: #555;
  animation: gt-15-bounce 2s ease-in-out infinite;
}
.gt-15__arrow {
  font-size: 1.5rem;
  color: #444;
  animation: gt-15-bounce 2s ease-in-out infinite .3s;
}
.gt-15__section {
  min-height: 60vh;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  max-width: 900px;
}
.gt-15__reveal {
  font-size: clamp(3rem, 10vw, 7rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -.01em;
  color: #2a2a2a;
  background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  opacity: 0;
  transform: translateY(40px);
  transition: opacity .8s ease, transform .8s ease;
  background-position: 100% center;
}
.gt-15__reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
  background-position: 0% center;
  transition: opacity .8s ease, transform .8s ease, background-position 1.2s ease;
}
.gt-15__reveal:nth-child(1) { transition-delay: 0s; }
.gt-15__reveal:nth-child(2) { transition-delay: .1s; }
@keyframes gt-15-bounce {
  0%, 100% { transform: translateY(0); }
  50%       { transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  .gt-15__reveal {
    opacity: 1;
    transform: none;
    transition: none;
    background-position: 0% center;
  }
  .gt-15__hint, .gt-15__arrow { animation: none; }
}
(function() {
  const items = document.querySelectorAll('.gt-15 .gt-15__reveal');
  if (!items.length) return;
  const io = new IntersectionObserver((entries) => {
    entries.forEach(e => {
      if (e.isIntersecting) {
        e.target.classList.add('is-visible');
      }
    });
  }, { threshold: 0.3 });
  items.forEach(el => io.observe(el));
})();

How this works

Headline elements start with opacity: 0, transform: translateY(40px), and background-position: 100% center — the gradient is off-screen to the right, appearing washed out. An IntersectionObserver with threshold: 0.3 watches each element and adds the .is-visible class when 30% of the element enters the viewport.

The .is-visible rule transitions opacity and transform in 0.8s, and simultaneously transitions background-position from 100% to 0% in 1.2s — a slightly longer duration so the colour washes in after the element has settled into place, creating a two-stage reveal effect.

Customize

  • Change the threshold to 0.1 for an earlier trigger — the gradient starts washing in as soon as 10% of the element is visible.
  • Add io.unobserve(e.target) after adding is-visible to fire the animation only once per element rather than every time it enters the viewport.
  • Adjust translateY(40px) to translateX(-40px) for a horizontal slide-in that differs from the standard vertical reveal.

Watch out for

  • background-position transition requires the element to have an explicit background-size set; the default auto size makes the position animation non-functional in some browsers.
  • IntersectionObserver does not fire for elements already in the viewport at page load on some browsers — call io.observe after a requestAnimationFrame delay if initial elements fail to reveal.
  • prefers-reduced-motion should immediately set opacity: 1 and transform: none without transition, so content is accessible to users with vestibular disorders even when JavaScript is active.

Browser support

ChromeSafariFirefoxEdge
58+ 12.1+ 55+ 58+

IntersectionObserver is well-supported in all modern browsers; a simple scroll-event fallback can serve IE11.

Search CodeFronts

Loading…