Typewriter
Text types itself in, one character at a time, with a blinking cursor and a touch of human unevenness. Give it several phrases and it types, erases and retypes each one.
How to use it
- Paste the snippet just before
</head>. - Add
data-typewriterto a heading to type its text, or give it phrases separated by|to cycle through them.
Options
Add any of these attributes to the same element.
data-typewriter="A|B|C" | Phrases to type, erase and retype in turn. |
data-loop | Keep retyping a single phrase. |
data-speed | Time per character. Default 60 ms. |
data-delete-speed | Time per character when erasing. Default 32 ms. |
data-hold | How long a finished phrase stays. Default 1600 ms. |
data-cursor="none" | Hide the cursor. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Typewriter: text types itself in, character by character, with a blinking
// cursor. Give it several phrases and it types, erases and retypes each one.
// Usage: <h1 data-typewriter>Hello, world</h1>
// or: <h1 data-typewriter="Design|Build|Ship"></h1> (cycles phrases)
// Optional: data-speed (ms per character), data-delete-speed (ms), data-hold
// (ms a phrase stays), data-loop (retype a single phrase), data-cursor ("none"
// to hide it).
var DEFAULTS = { speed: 60, deleteSpeed: 32, hold: 1600 };
var css =
'.tw-cursor{display:inline-block;width:0.08em;height:1em;margin-left:0.06em;vertical-align:-0.12em;background:currentColor;border-radius:1px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var list = el.getAttribute('data-typewriter');
var phrases = (list ? list.split('|') : [el.textContent]).map(function (p) { return p.replace(/\s+/g, ' ').trim(); }).filter(Boolean);
el.setAttribute('aria-label', phrases.join(', '));
el.textContent = '';
var text = document.createElement('span');
text.setAttribute('aria-hidden', 'true');
el.appendChild(text);
var cursor = null;
if (el.dataset.cursor !== 'none') {
cursor = document.createElement('span');
cursor.className = 'tw-cursor';
cursor.setAttribute('aria-hidden', 'true');
el.appendChild(cursor);
}
return { el: el, phrases: phrases, text: text, cursor: cursor };
}
function run(t) {
var d = t.el.dataset;
var speed = +d.speed || DEFAULTS.speed;
var delSpeed = +d.deleteSpeed || DEFAULTS.deleteSpeed;
var hold = +d.hold || DEFAULTS.hold;
var cycle = t.phrases.length > 1 || (d.loop !== undefined && d.loop !== 'false');
var blink = null;
function blinkOn() {
if (!t.cursor || blink) return;
blink = anime.animate(t.cursor, { opacity: [1, 0], duration: 530, ease: 'linear', loop: true, alternate: true });
}
function blinkOff() {
if (blink) { blink.revert(); blink = null; }
}
var index = 0;
function type(phrase, i) {
blinkOff();
if (i <= phrase.length) {
t.text.textContent = phrase.slice(0, i);
// A little human unevenness: pauses after spaces and punctuation.
var pause = /[ ,.!?]/.test(phrase[i - 1] || '') ? speed * 2.2 : speed * anime.utils.random(0.6, 1.4, 2);
return setTimeout(function () { type(phrase, i + 1); }, pause);
}
blinkOn();
if (cycle) setTimeout(function () { erase(phrase.length); }, hold);
}
function erase(i) {
blinkOff();
if (i >= 0) {
t.text.textContent = t.phrases[index].slice(0, i);
return setTimeout(function () { erase(i - 1); }, delSpeed);
}
index = (index + 1) % t.phrases.length;
setTimeout(function () { type(t.phrases[index], 1); }, speed * 6);
}
blinkOn();
setTimeout(function () { type(t.phrases[0], 1); }, 400);
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
run(e.target._tw);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-typewriter]').forEach(function (el) {
if (el._tw) return;
el._tw = setup(el);
if (reduced) { el._tw.text.textContent = el._tw.phrases[0]; return; }
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-typewriter>Hello, world</h1>
<!-- cycle through phrases: -->
<h1 data-typewriter="Design it|Build it|Ship it"></h1>