Text types itself out with a blinking caret.
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;
}
}