Select and Retype
A caret drags across the word in your sentence, highlighting it like selected text. A beat later the next word is typed straight over the selection, letter by letter, with a blinking caret.
We write code
How to use it
- Paste the snippet just before
</head>. - Wrap the changing word in a
<span>and list every word, separated by commas, indata-select-and-retype.
Options
Add any of these attributes to the same element.
data-hold | How long each word stays. Default 1800 ms. |
data-speed | Typing time per letter (it varies a little, like a real typist). Default 85 ms. |
data-select-speed | Time per letter while the selection drags across. Default 45 ms; 0 selects the whole word at once, like a double-click. |
data-select-color | The selection highlight. Default a soft system blue. |
data-caret-color | The caret’s colour. Default the word’s. |
The word takes its natural width, so the sentence closes up and opens out as it’s retyped, just as it would in an editor. Typed Word Rotator backspaces instead.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Select and Retype: the word in your sentence gets selected like
// highlighted text, as if someone dragged across it, then the next word is
// typed over the selection with a blinking caret. The sentence reflows
// around the word as it changes length.
// Usage: <h1>We write <span data-select-and-retype="code, copy, stories">code</span></h1>
// Optional: data-hold (ms per word, default 1800), data-speed (ms per typed
// letter, default 85), data-select-speed (ms per letter while selecting,
// default 45; 0 selects the whole word at once, like a double-click),
// data-select-color (default a soft system blue), data-caret-color
// (default the word's colour).
var DEFAULTS = { hold: 1800, speed: 85, selectSpeed: 45, selectColor: 'rgba(56,132,255,.3)' };
var css =
'.sar{white-space:nowrap}' +
'.sar-sel{border-radius:.06em;box-shadow:0 0 0 .02em transparent}' +
'.sar-caret{display:inline-block;position:relative;width:0;height:0}' +
'.sar-caret::after{content:"";position:absolute;left:-.02em;bottom:-.14em;height:1em;border-left:.06em solid currentColor}' +
'.sar-blink .sar-caret::after{animation:sar-blink 1s steps(1) infinite}' +
'@keyframes sar-blink{50%{opacity:0}}' +
'.sar-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function span(cls, parent, text) {
var s = document.createElement('span');
s.className = cls;
if (text) s.textContent = text;
parent.appendChild(s);
return s;
}
function setup(el) {
var words = (el.getAttribute('data-select-and-retype') || el.textContent)
.split(',').map(function (w) { return w.trim(); }).filter(Boolean);
var d = el.dataset;
el.classList.add('sar', 'sar-blink');
el.textContent = '';
span('sar-sr', el, words.join(', '));
var vis = span('sar-vis', el);
vis.setAttribute('aria-hidden', 'true');
var sel = span('sar-sel', vis);
var caret = span('sar-caret', vis);
var rest = span('sar-rest', vis, words[0]);
sel.style.background = d.selectColor || DEFAULTS.selectColor;
if (d.caretColor) caret.style.color = d.caretColor;
caret.style.opacity = '0';
return { el: el, words: words, sel: sel, caret: caret, rest: rest, i: 0, visible: false, started: false };
}
// Draw the word as [selected part][caret][rest].
function draw(s, selected, rest) {
s.sel.textContent = selected;
s.rest.textContent = rest;
}
// Run each(0..n), ms apart (ms may be a function, for a human rhythm), then done.
function steps(n, ms, each, done) {
var k = 0;
(function tick() {
if (k > n) return done();
each(k++);
setTimeout(tick, typeof ms === 'function' ? ms() : ms);
})();
}
function swap(s, done) {
var d = s.el.dataset;
var speed = +d.speed || DEFAULTS.speed;
var selSpeed = d.selectSpeed !== undefined ? +d.selectSpeed : DEFAULTS.selectSpeed;
var from = s.words[s.i];
s.i = (s.i + 1) % s.words.length;
var to = s.words[s.i];
// 1. The caret lands at the start and drags across the word.
s.el.classList.remove('sar-blink');
s.caret.style.opacity = '1';
draw(s, '', from);
var drag = selSpeed > 0 ? from.length : 1;
setTimeout(function () {
steps(drag, selSpeed || 0, function (k) {
var n = selSpeed > 0 ? k : (k ? from.length : 0);
draw(s, from.slice(0, n), from.slice(n));
}, function () {
// 2. A beat with the word selected, then typing replaces it.
s.el.classList.add('sar-blink');
setTimeout(function () {
s.el.classList.remove('sar-blink');
steps(to.length - 1, function () { return speed * (0.6 + Math.random() * 0.8); }, function (k) {
typed(s, to.slice(0, k + 1));
}, function () {
// 3. The caret blinks a moment, then fades away.
s.el.classList.add('sar-blink');
setTimeout(function () {
s.caret.style.opacity = '0';
done();
}, 900);
});
}, 450);
});
}, 250);
}
// Typed text sits before the caret, unselected.
function typed(s, text) {
s.sel.style.background = 'none';
s.sel.textContent = text;
s.rest.textContent = '';
}
function resetSel(s) {
s.sel.style.background = s.el.dataset.selectColor || DEFAULTS.selectColor;
}
// Wait, then run fn, but only once the word is on screen.
function wait(s, ms, fn) {
setTimeout(function check() {
if (s.visible && !document.hidden) fn();
else setTimeout(check, 300);
}, ms);
}
function start(s) {
var hold = +s.el.dataset.hold || DEFAULTS.hold;
(function loop() {
wait(s, hold, function () {
// The caret is gone and the word is plain text again: move it back
// after the caret so the next drag starts from the left.
var word = s.sel.textContent + s.rest.textContent;
resetSel(s);
draw(s, '', word);
swap(s, loop);
});
})();
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._sar;
s.visible = e.isIntersecting;
if (s.visible && !s.started) { s.started = true; start(s); }
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-select-and-retype]').forEach(function (el) {
if (el._sar) return;
el._sar = setup(el);
if (!reduced && el._sar.words.length > 1) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1>
We write
<span data-select-and-retype="code, copy, stories">code</span>
</h1>