Typewriter

kinetic

Text types itself out with a blinking caret.

I Love Bangladesh

17/120
Align
Ease
Duration
s
0.13
Stagger
s
00.2
Delay
s
02
export function typewriter(el) {
  const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const _duration = _reducedMotion ? 0.01 : 1.5
  const _delay = _reducedMotion ? 0 : 0

  const text = el.textContent ?? ''
  el.textContent = ''
  el.classList.add('k-type-caret')
  const points = [0, 1]
  const durationMs = _duration * 1000
  const delayMs = _delay * 1000
  let raf = 0
  let start = null
  let cancelled = false

  const tick = (now) => {
    if (cancelled) return
    if (start === null) start = now
    const elapsed = now - start - delayMs
    if (elapsed < 0) {
      raf = requestAnimationFrame(tick)
      return
    }
    const t = Math.min(1, elapsed / durationMs)
    const count = Math.round(easeAt(points, t) * text.length)
    el.textContent = text.slice(0, count)
    if (t < 1) {
      raf = requestAnimationFrame(tick)
    } else {
    }
  }
  raf = requestAnimationFrame(tick)

  return () => {
    cancelled = true
    cancelAnimationFrame(raf)
    el.classList.remove('k-type-caret')
    el.textContent = text
  }
}

function easeAt(points, t) {
  const clamped = t < 0 ? 0 : t > 1 ? 1 : t
  const scaled = clamped * (points.length - 1)
  const i = Math.floor(scaled)
  const frac = scaled - i
  const a = points[i]
  const b = points[Math.min(i + 1, points.length - 1)]
  return a + (b - a) * frac
}

/* CSS */
.k-type-caret::after {
  content: "|";
  display: inline-block;
  margin-inline-start: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .k-type-caret::after {
    animation: k-caret-blink 1s steps(1) infinite;
  }
}

@keyframes k-caret-blink {
  50% {
    opacity: 0;
  }
}