Keystroke Overlay
Pressed keys pop up as keycaps over the screen, like the key display in a tutorial video. Shortcuts show as a row of caps, plain typing gathers into one capsule, and older presses drift up and fade. Play a scripted sequence, show the visitor’s own key presses, or both.
Press any keys
How to use it
- Paste the snippet just before
</head>. - Add
data-keystroke-overlayto the area the keys should appear over. - List a sequence in
data-keysand adddata-autoplayto play it, and/ordata-listento show real key presses.
Options
Add any of these attributes to the same element.
data-keys | The sequence: steps separated by |, keys in a shortcut joined by +. A step starting with ~ types its text letter by letter, e.g. ⌘+K|~deploy|↵. |
data-autoplay | Play the sequence on a loop while it is on screen. |
data-listen | Show the visitor’s own key presses while it is on screen. The sequence rests while they type. |
data-position | bottom (default) or top. |
data-interval | Time between steps of the sequence. Default 1000 ms. |
data-hold | How long a press stays before it fades. Default 1800 ms. |
data-max | How many presses show at once. Default 3. |
Key names like Cmd, Shift and Enter are drawn as their symbols (⌘, ⇧, ↵). From a script, animacious.keystrokeOverlay(el, ['⌘', 'S']) shows a single press.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Keystroke Overlay: pressed keys pop up as keycaps over the screen, like the
// key display in a tutorial video. Shortcuts show as a row of keycaps; plain
// typing gathers into one capsule. Older presses drift up and fade away.
// Usage:
// <div data-keystroke-overlay data-autoplay data-keys="⌘+K|~deploy|↵|Esc">
// …anything: the "screen" the keys appear over…
// </div>
// Optional: data-keys (a sequence to play: steps separated by |, keys in a
// shortcut joined by +, and a step starting with ~ types its text letter by
// letter), data-autoplay (play the sequence on a loop while in view),
// data-listen (show the visitor's own key presses while in view), data-position
// ("bottom", default, or "top"), data-interval (ms between steps, default 1000),
// data-hold (ms a press stays before fading, default 1800), data-max (presses
// shown at once, default 3).
// Script use: animacious.keystrokeOverlay(el, ['⌘', 'K']) shows one press.
var DEFAULTS = { position: 'bottom', interval: 1000, hold: 1800, max: 3 };
var MAC = /Mac|iP(hone|ad|od)/.test(navigator.platform || navigator.userAgent);
var NAMES = {
Meta: MAC ? '⌘' : 'Win', Control: MAC ? '⌃' : 'Ctrl', Alt: MAC ? '⌥' : 'Alt', Shift: '⇧',
Enter: '↵', Backspace: '⌫', Delete: 'Del', Tab: '⇥', Escape: 'Esc', ' ': 'Space',
ArrowUp: '↑', ArrowDown: '↓', ArrowLeft: '←', ArrowRight: '→', CapsLock: '⇪'
};
var MODS = ['Control', 'Alt', 'Shift', 'Meta'];
var css =
'.kso-layer{position:absolute;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:0.45em;pointer-events:none;z-index:5;padding:0 0.8em}' +
'.kso-bottom{bottom:1.1em}.kso-top{top:1.1em}' +
'.kso-group{display:flex;gap:0.3em;padding:0.35em;border-radius:0.75em;background:rgba(18,18,26,.78);' +
'-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 10px 30px rgba(0,0,0,.25);max-width:100%}' +
'.kso-cap{min-width:2.1em;height:2.1em;padding:0 0.55em;display:grid;place-items:center;border-radius:0.5em;' +
'background:linear-gradient(#3a3a46,#2a2a34);color:#fff;font-weight:600;font-size:1em;line-height:1;white-space:nowrap;' +
'box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 2px 0 #111118;font-family:inherit}' +
'.kso-text{display:flex;align-items:center;min-width:0;justify-content:flex-start;overflow:hidden;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:500;letter-spacing:.02em}' +
'.kso-text i{font-style:normal;display:inline-block;white-space:pre}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function opt(d, k) { return d[k] !== undefined && d[k] !== '' ? d[k] : DEFAULTS[k]; }
function label(k) {
if (NAMES[k]) return NAMES[k];
var low = k.toLowerCase();
if (low === 'cmd' || low === 'command') return '⌘';
if (low === 'opt' || low === 'option') return '⌥';
if (low === 'plus') return '+';
return k.length === 1 ? k.toUpperCase() : k;
}
function setup(el) {
var d = el.dataset;
if (getComputedStyle(el).position === 'static') el.style.position = 'relative';
var layer = document.createElement('div');
layer.className = 'kso-layer kso-' + (opt(d, 'position') === 'top' ? 'top' : 'bottom');
layer.setAttribute('aria-hidden', 'true');
el.appendChild(layer);
var steps = (d.keys || '').split('|').filter(Boolean).map(function (s) {
return s.charAt(0) === '~' ? { text: s.slice(1) } : { keys: s.split(/\+(?!$)/).map(function (k) { return k.trim(); }) };
});
return { el: el, layer: layer, steps: steps, groups: [], visible: false, timer: 0, idleUntil: 0, last: null };
}
// Move older groups smoothly when a new one joins (FLIP).
function addGroup(s, g) {
var before = s.groups.map(function (o) { return o.el.getBoundingClientRect().top; });
if (s.layer.classList.contains('kso-top')) s.layer.insertBefore(g.el, s.layer.firstChild);
else s.layer.appendChild(g.el);
if (s.still) { s.groups.push(g); return; }
s.groups.forEach(function (o, i) {
var dy = before[i] - o.el.getBoundingClientRect().top;
if (dy) anime.animate(o.el, { y: [dy, 0], duration: 320, ease: 'out(3)' });
});
s.groups.push(g);
var max = +opt(s.el.dataset, 'max');
while (s.groups.length > max) drop(s, s.groups[0]);
anime.animate(g.el, { opacity: [0, 1], scale: [0.7, 1], y: [10, 0], duration: 420, ease: 'outBack(1.8)' });
}
function drop(s, g) {
var i = s.groups.indexOf(g);
if (i < 0) return;
s.groups.splice(i, 1);
clearTimeout(g.timer);
anime.animate(g.el, {
opacity: 0, scale: 0.85, y: s.layer.classList.contains('kso-top') ? 8 : -8, duration: 300, ease: 'in(2)',
onComplete: function () { g.el.remove(); }
});
}
function keepAlive(s, g) {
clearTimeout(g.timer);
g.timer = setTimeout(function () { drop(s, g); }, +opt(s.el.dataset, 'hold'));
}
function pop(s, node) {
if (s.still) return;
anime.animate(node, { scale: [1.35, 1], y: [-3, 0], duration: 260, ease: 'out(3)' });
}
// Show a shortcut as keycaps.
function showCombo(s, keys) {
var g = { el: document.createElement('div') };
g.el.className = 'kso-group';
keys.forEach(function (k) {
var cap = document.createElement('span');
cap.className = 'kso-cap';
cap.textContent = label(k);
g.el.appendChild(cap);
});
addGroup(s, g);
if (!s.still) anime.animate(g.el.children, { y: [0, 2, 0], duration: 220, delay: anime.stagger(50, { start: 120 }), ease: 'inOut(2)' });
keepAlive(s, g);
s.last = null;
}
// Show a typed character, joining the previous capsule if it was typing too.
function showChar(s, ch) {
var g = s.last && s.groups.indexOf(s.last) >= 0 ? s.last : null;
if (!g) {
g = { el: document.createElement('div'), text: document.createElement('span') };
g.el.className = 'kso-group';
g.text.className = 'kso-cap kso-text';
g.el.appendChild(g.text);
addGroup(s, g);
s.last = g;
}
var i = document.createElement('i');
i.textContent = ch;
g.text.appendChild(i);
// Keep the capsule short: only the latest letters stay.
while (g.text.children.length > 16) g.text.firstChild.remove();
pop(s, i);
keepAlive(s, g);
}
function schedule(s, ms) {
clearTimeout(s.timer);
s.timer = setTimeout(function () { tick(s); }, ms);
}
function tick(s) {
s.timer = 0;
if (!s.visible || !s.steps.length) return;
if (Date.now() < s.idleUntil) { schedule(s, s.idleUntil - Date.now()); return; }
var interval = +opt(s.el.dataset, 'interval');
s.pos = s.pos || { i: 0, c: 0 };
var step = s.steps[s.pos.i % s.steps.length];
if (step.text !== undefined) {
showChar(s, step.text.charAt(s.pos.c++));
if (s.pos.c < step.text.length) { schedule(s, 90 + Math.random() * 90); return; }
} else {
showCombo(s, step.keys);
}
s.pos.c = 0;
s.pos.i++;
// A longer rest after the whole sequence, before it starts over.
schedule(s, s.pos.i % s.steps.length === 0 ? interval + +opt(s.el.dataset, 'hold') : interval);
}
function listen(list) {
window.addEventListener('keydown', function (e) {
if (e.repeat || MODS.indexOf(e.key) >= 0 || e.key === 'Dead' || e.key === 'Unidentified') return;
list.forEach(function (s) {
if (!s.visible) return;
s.idleUntil = Date.now() + 4000; // the visitor is typing: rest the demo
var mods = MODS.filter(function (m) { return e.getModifierState(m); });
var plain = !mods.filter(function (m) { return m !== 'Shift'; }).length;
var typing = s.last && s.groups.indexOf(s.last) >= 0;
if (plain && e.key.length === 1 && (e.key !== ' ' || typing)) showChar(s, e.key);
else showCombo(s, mods.concat(e.key));
});
});
}
function init() {
var listeners = [];
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._kso;
s.visible = e.isIntersecting;
if (s.visible && s.autoplay && !s.timer) schedule(s, 500);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-keystroke-overlay]').forEach(function (el) {
if (el._kso) return;
var s = (el._kso = setup(el));
var d = el.dataset;
s.autoplay = d.autoplay !== undefined && d.autoplay !== 'false';
if (d.listen !== undefined && d.listen !== 'false') listeners.push(s);
if (reduced()) {
// A single still press, no motion.
s.still = true;
s.autoplay = false;
if (s.steps.length && s.steps[0].keys) {
showCombo(s, s.steps[0].keys);
s.groups.forEach(function (g) { clearTimeout(g.timer); });
}
}
io.observe(el);
});
if (listeners.length) listen(listeners);
}
window.animacious = window.animacious || {};
window.animacious.keystrokeOverlay = function (el, keys) {
if (el && el._kso) showCombo(el._kso, keys);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-keystroke-overlay data-autoplay data-keys="⌘+K|~deploy|↵|Esc">
<!-- your screen, video or screenshot -->
</div>