Placeholder Typewriter
A search box’s placeholder types out example searches one after another, erasing each before the next, so visitors see what they can ask for. The moment someone clicks into the box or starts typing, it steps aside.
How to use it
- Paste the snippet just before
</head>. - Add
data-placeholder-typewriterto an<input>or<textarea>, with example searches separated by|.
Options
Add any of these attributes to the same element.
data-placeholder-typewriter="A|B|C" | The examples to type, in turn. Without a value it uses the existing placeholder. |
data-prefix | Text that stays in front of every example, like Try . |
data-speed | Typing time per character. Default 70 ms. |
data-delete-speed | Time per character when erasing. Default 28 ms. |
data-hold | How long each finished example stays. Default 1600 ms. |
data-caret="false" | Don’t show a caret. |
It stops while the box has focus or any text, and starts again when it is empty and left. Give the input an aria-label (or a <label>), as screen readers don’t follow a changing placeholder. It also works on a plain element styled to look like a box.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Placeholder Typewriter: a search box's placeholder types out example
// queries one after another, erasing each before the next. It steps aside
// the moment someone focuses the box or types in it.
// Usage: <input type="search" aria-label="Search"
// data-placeholder-typewriter="Lemon pasta|Quick weeknight curry|Vegan brownies">
// It also works on any other element (a <span> styled as a fake box), where
// it types into the element's text with a blinking caret.
// Optional: data-prefix (text that stays in front, e.g. "Try "), data-speed
// (ms per character, default 70), data-delete-speed (ms per character when
// erasing, default 28), data-hold (ms a finished phrase stays, default 1600),
// data-caret="false" (no caret).
var DEFAULTS = { speed: 70, deleteSpeed: 28, hold: 1600 };
var css =
'.phw-caret{display:inline-block;width:2px;height:1.1em;margin-left:1px;vertical-align:-0.15em;background:currentColor;opacity:.7}' +
'.phw-caret.phw-blink{animation:phw-blink 1.06s steps(1) infinite}@keyframes phw-blink{50%{opacity:0}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var isField = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
var phrases = (d.placeholderTypewriter || el.getAttribute('placeholder') || el.textContent).split('|')
.map(function (p) { return p.trim(); }).filter(Boolean);
var s = {
el: el, isField: isField, phrases: phrases, prefix: d.prefix || '', caretOn: d.caret !== 'false',
speed: +d.speed || DEFAULTS.speed, del: +d.deleteSpeed || DEFAULTS.deleteSpeed, hold: +d.hold || DEFAULTS.hold,
i: 0, n: 0, phase: 'type', timer: 0, visible: false, blinkOn: true
};
if (isField) {
if (!el.getAttribute('aria-label') && !(el.labels && el.labels.length)) el.setAttribute('aria-label', s.prefix + phrases[0]);
el.addEventListener('focus', function () { stop(s); show(s, phrases[s.i % phrases.length]); });
el.addEventListener('blur', function () { if (!el.value) resume(s); });
el.addEventListener('input', function () { if (el.value) stop(s); });
} else {
el.setAttribute('aria-label', s.prefix + phrases[0]);
el.textContent = '';
s.text = document.createElement('span');
s.text.setAttribute('aria-hidden', 'true');
el.appendChild(s.text);
if (s.caretOn) {
s.caret = document.createElement('span');
s.caret.className = 'phw-caret';
s.caret.setAttribute('aria-hidden', 'true');
el.appendChild(s.caret);
}
}
return s;
}
// Write the text (and caret) into the placeholder or the element.
function show(s, text, caret) {
if (s.isField) {
s.el.setAttribute('placeholder', s.prefix + text + (caret && s.caretOn ? '|' : ''));
} else {
s.text.textContent = s.prefix + text;
if (s.caret) s.caret.classList.toggle('phw-blink', !!caret && caret === 'blink');
}
}
function stop(s) { clearTimeout(s.timer); s.timer = 0; s.stopped = true; }
function resume(s) {
if (!s.stopped) return;
s.stopped = false;
s.phase = 'erase';
s.n = s.phrases[s.i % s.phrases.length].length;
if (s.visible) step(s);
}
function busy(s) { return s.isField && (document.activeElement === s.el || s.el.value); }
function step(s) {
clearTimeout(s.timer);
s.timer = 0;
if (s.stopped || !s.visible || busy(s)) return;
var phrase = s.phrases[s.i % s.phrases.length];
var wait;
if (s.phase === 'type') {
show(s, phrase.slice(0, ++s.n), true);
wait = s.speed * (0.6 + Math.random() * 0.8);
if (s.n >= phrase.length) {
if (s.phrases.length < 2) { show(s, phrase, 'blink'); return; }
s.phase = 'hold';
s.ticks = Math.max(1, Math.round(s.hold / 530));
wait = 530;
}
} else if (s.phase === 'hold') {
// Blink the caret while the phrase rests.
s.blinkOn = !s.blinkOn;
if (s.isField) show(s, phrase, s.blinkOn);
else show(s, phrase, 'blink');
wait = 530;
if (--s.ticks <= 0) s.phase = 'erase';
} else {
show(s, phrase.slice(0, --s.n), true);
wait = s.del;
if (s.n <= 0) { s.n = 0; s.i++; s.phase = 'type'; wait = 380; }
}
s.timer = setTimeout(function () { step(s); }, wait);
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._phw;
s.visible = e.isIntersecting;
if (s.visible && !s.timer) step(s);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-placeholder-typewriter]').forEach(function (el) {
if (el._phw) return;
var s = (el._phw = setup(el));
if (reduced()) { show(s, s.phrases[0]); return; }
show(s, '', true);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><input type="search" aria-label="Search recipes"
data-placeholder-typewriter="lemon pasta|quick weeknight curry|vegan brownies"
data-prefix="Try ">