20 CSS Gradient Text Designs 18 / 20

CSS Typewriter Loop Gradient Text Animation

A typewriter loop cycles through a curated list of words letter by letter, each rendered in a continuously scrolling gradient, with a blinking cursor and word chip indicators.

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

The code

<div class="gt-18">
  <span class="gt-18__label">Typewriter gradient loop</span>
  <p class="gt-18__prefix">We design &nbsp;&nbsp;→</p>
  <div class="gt-18__stage">
    <span class="gt-18__typed" id="gt-18-typed">WEBSITES</span><span class="gt-18__cursor" aria-hidden="true">|</span>
  </div>
  <div class="gt-18__words-display" id="gt-18-chips"></div>
</div>
<script>
(function() {
  const words = ['WEBSITES', 'PRODUCTS', 'SYSTEMS', 'BRANDS', 'FUTURES', 'MOTION'];
  const typed = document.getElementById('gt-18-typed');
  const chipsEl = document.getElementById('gt-18-chips');
  if (!typed) return;
  // Build chips
  words.forEach((w, i) => {
    const chip = document.createElement('span');
    chip.className = 'gt-18__word-chip' + (i === 0 ? ' is-active' : '');
    chip.textContent = w;
    chip.id = 'gt-18-chip-' + i;
    chipsEl.appendChild(chip);
  });
  let cur = 0;
  function setActive(idx) {
    document.querySelectorAll('.gt-18 .gt-18__word-chip').forEach((c,i) => {
      c.classList.toggle('is-active', i === idx);
    });
  }
  async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  async function typeWord(word) {
    for (let i = 1; i <= word.length; i++) {
      typed.textContent = word.slice(0, i);
      await sleep(80);
    }
    await sleep(1400);
    for (let i = word.length; i > 0; i--) {
      typed.textContent = word.slice(0, i - 1);
      await sleep(50);
    }
  }
  async function loop() {
    while (true) {
      setActive(cur);
      await typeWord(words[cur]);
      cur = (cur + 1) % words.length;
    }
  }
  loop();
})();
</script>
.gt-18, .gt-18 *, .gt-18 *::before, .gt-18 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.gt-18 {
  --bg: #0a0e18;
  font-family: 'JetBrains Mono', monospace;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3rem;
  padding: 3rem 2rem;
}
.gt-18__label {
  font-size: .65rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: #1a2a4a;
}
.gt-18__prefix {
  font-size: clamp(1rem, 3vw, 1.5rem);
  font-weight: 700;
  color: #3a5a8a;
  letter-spacing: .05em;
}
.gt-18__stage {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
  justify-content: center;
}
.gt-18__typed {
  font-size: clamp(3rem, 11vw, 7rem);
  font-weight: 800;
  line-height: 1;
  background: linear-gradient(90deg, #4facfe 0%, #00f2fe 50%, #a855f7 100%);
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: gt-18-gradshift 3s linear infinite;
  min-width: 2ch;
  display: inline-block;
}
.gt-18__cursor {
  font-size: clamp(3rem, 11vw, 7rem);
  font-weight: 800;
  line-height: 1;
  color: #4facfe;
  animation: gt-18-blink .8s step-end infinite;
  display: inline-block;
  margin-left: 2px;
}
.gt-18__words-display {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  justify-content: center;
}
.gt-18__word-chip {
  font-size: .7rem;
  letter-spacing: .1em;
  padding: .3em .7em;
  border-radius: 4px;
  border: 1px solid #1a3060;
  color: #3a5a8a;
  transition: border-color .3s, color .3s;
}
.gt-18__word-chip.is-active {
  border-color: #4facfe;
  color: #4facfe;
  background: #4facfe10;
}
@keyframes gt-18-gradshift {
  0%   { background-position: 0% center; }
  100% { background-position: 200% center; }
}
@keyframes gt-18-blink {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .gt-18__typed { animation: none; background-position: 0% center; }
  .gt-18__cursor { animation: none; }
}
(function() {
  const words = ['WEBSITES', 'PRODUCTS', 'SYSTEMS', 'BRANDS', 'FUTURES', 'MOTION'];
  const typed = document.getElementById('gt-18-typed');
  const chipsEl = document.getElementById('gt-18-chips');
  if (!typed) return;
  // Build chips
  words.forEach((w, i) => {
    const chip = document.createElement('span');
    chip.className = 'gt-18__word-chip' + (i === 0 ? ' is-active' : '');
    chip.textContent = w;
    chip.id = 'gt-18-chip-' + i;
    chipsEl.appendChild(chip);
  });
  let cur = 0;
  function setActive(idx) {
    document.querySelectorAll('.gt-18 .gt-18__word-chip').forEach((c,i) => {
      c.classList.toggle('is-active', i === idx);
    });
  }
  async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  async function typeWord(word) {
    for (let i = 1; i <= word.length; i++) {
      typed.textContent = word.slice(0, i);
      await sleep(80);
    }
    await sleep(1400);
    for (let i = word.length; i > 0; i--) {
      typed.textContent = word.slice(0, i - 1);
      await sleep(50);
    }
  }
  async function loop() {
    while (true) {
      setActive(cur);
      await typeWord(words[cur]);
      cur = (cur + 1) % words.length;
    }
  }
  loop();
})();

How this works

JavaScript manages the typing and deleting loop asynchronously using an async/await sleep() pattern. Each character is added or removed by setting textContent to a slice of the target word. The typed element always has its gradient animation running — gt-18-gradshift scrolling a 200% 100% background indefinitely — so letters materialise already coloured with the moving gradient rather than flashing in then transitioning.

The blinking cursor is a sibling span with animation: gt-18-blink .8s step-end infinite. The step-end timing function creates the hard on/off blink of a real terminal cursor rather than a fade. Word chip indicators use the .is-active class toggled by JavaScript to show which word is currently being typed.

Customize

  • Adjust typing speed by changing the 80ms per-character delay in the typeWord function — lower values feel machine-fast, higher values feel deliberate.
  • Add a random jitter to the typing delay (60 + Math.random() * 60ms) for a more human-like unpredictable cadence.
  • Change the gradient on the typed element by updating the background-image at the start of each word cycle to match a colour palette for that specific word.

Watch out for

  • Setting textContent on an element that contains child nodes (like the cursor span) will delete those children — keep the typed text and cursor in sibling elements, never nested.
  • The async loop has no cancellation mechanism — if the component is removed from the DOM mid-cycle, the loop continues writing to detached nodes. Use an AbortController or a running flag to stop the loop on cleanup.
  • The gradient is clipped to the text character bounding box — a min-width: 2ch on the typed element prevents layout shift when the visible text goes to zero characters during delete.

Browser support

ChromeSafariFirefoxEdge
58+ 12.1+ 55+ 58+

All features are well-supported in modern browsers; the async/await pattern requires Safari 10.1+ (native async support).

Search CodeFronts

Loading…