Terminal Print
A command-line session plays itself out: commands are typed after the prompt with a block cursor, and their output prints a beat later. Great for developer tools and docs.
npm run build
✓ 48 animations bundled
npm run deploy
✓ Live in 1.2s
How to use it
- Paste the snippet just before
</head>. - Add
data-terminal-printto a container with one element per line. Mark commands withdata-cmd; every other line is output.
Options
Add any of these attributes to the same element.
data-prompt | The prompt before each command. Default $. |
data-speed | Typing time per character. Default 45 ms. |
data-title | Text in the window’s title bar. Default “Terminal”. |
data-chrome="false" | Hide the window frame and its three dots. |
data-loop | Clear and replay after a pause. |
Every line’s space is reserved up front, so the box never grows while it plays.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Terminal Print: a command-line session plays out. Commands are typed after
// a prompt with a block cursor; their output prints a beat later.
// Usage:
// <div data-terminal-print>
// <p data-cmd>npm install animacious</p>
// <p>added 1 package in 0.4s</p>
// </div>
// Lines with data-cmd are typed; the others print instantly.
// Optional: data-prompt (default "$"), data-speed (ms per character, default
// 45), data-title (window title), data-chrome="false" (no window frame),
// data-loop (clear and replay).
var DEFAULTS = { prompt: '$', speed: 45, outputDelay: 380, hold: 2600 };
var css =
'.tpx{background:#101015;color:#e4e4e7;border-radius:12px;overflow:hidden;text-align:left;box-shadow:0 18px 40px rgba(10,10,20,.28);' +
'font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
'.tpx-bar{display:flex;align-items:center;gap:6px;padding:0.7em 0.9em;background:#1b1b22;font-size:0.8em;color:#8a8a93}' +
'.tpx-bar i{width:0.8em;height:0.8em;border-radius:50%;background:#3f3f46}' +
'.tpx-bar i:nth-child(1){background:#ff5f57}.tpx-bar i:nth-child(2){background:#febc2e}.tpx-bar i:nth-child(3){background:#28c840}' +
'.tpx-bar span{margin-left:auto;margin-right:auto;padding-right:3em}' +
'.tpx-body{padding:0.9em 1.1em 1.1em}' +
'.tpx-body>*{margin:0;line-height:1.6;white-space:pre-wrap;min-height:1.6em}' +
'.tpx-out{color:#a1a1aa}' +
'.tpx-prompt{color:#4ade80;margin-right:0.6em}' +
'.tpx-cursor{display:inline-block;width:0.55em;height:1.15em;vertical-align:-0.2em;background:#e4e4e7;margin-left:1px}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var lines = Array.from(el.children).map(function (line) {
var cmd = line.hasAttribute('data-cmd');
return { el: line, cmd: cmd, text: line.textContent.trim() };
});
el.classList.add('tpx');
var body = document.createElement('div');
body.className = 'tpx-body';
lines.forEach(function (l) { body.appendChild(l.el); });
if (d.chrome !== 'false') {
var bar = document.createElement('div');
bar.className = 'tpx-bar';
bar.setAttribute('aria-hidden', 'true');
bar.innerHTML = '<i></i><i></i><i></i><span></span>';
bar.querySelector('span').textContent = d.title || 'Terminal';
el.appendChild(bar);
}
el.appendChild(body);
var cursor = document.createElement('span');
cursor.className = 'tpx-cursor';
cursor.setAttribute('aria-hidden', 'true');
var blink = anime.animate(cursor, { opacity: [1, 0], duration: 530, ease: 'linear', loop: true, alternate: true });
return { el: el, lines: lines, cursor: cursor, blink: blink, prompt: d.prompt || DEFAULTS.prompt };
}
// Put every line back to its finished state (used for reduced motion and
// as the layout reference, so the box never grows while it plays).
function render(t, full) {
t.lines.forEach(function (l) {
l.el.textContent = '';
l.el.classList.toggle('tpx-out', !l.cmd);
if (l.cmd) {
var p = document.createElement('span');
p.className = 'tpx-prompt';
p.textContent = t.prompt;
l.el.appendChild(p);
l.el.appendChild(document.createElement('span')).textContent = full ? l.text : '';
} else {
l.el.textContent = l.text;
}
l.el.style.visibility = full ? '' : 'hidden';
});
}
function play(t) {
var d = t.el.dataset;
var speed = +d.speed || DEFAULTS.speed;
render(t, false);
var i = 0;
function next() {
if (i >= t.lines.length) {
// Leave the cursor on a fresh prompt line, like a real shell.
var last = t.lines[t.lines.length - 1].el;
last.appendChild(t.cursor);
if (d.loop !== undefined && d.loop !== 'false') setTimeout(function () { play(t); }, DEFAULTS.hold);
return;
}
var l = t.lines[i++];
l.el.style.visibility = '';
if (!l.cmd) {
anime.animate(l.el, { opacity: [0, 1], duration: 200, ease: 'out(2)' });
setTimeout(next, 90);
return;
}
var out = l.el.lastChild;
l.el.appendChild(t.cursor);
var n = 0;
(function type() {
out.textContent = l.text.slice(0, n);
t.blink.pause();
t.cursor.style.opacity = 1;
if (n++ < l.text.length) { setTimeout(type, speed * (0.6 + Math.random() * 0.8)); return; }
t.blink.play();
setTimeout(next, DEFAULTS.outputDelay);
})();
}
setTimeout(next, 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);
play(e.target._tpx);
});
}, { threshold: 0.4 });
document.querySelectorAll('[data-terminal-print]').forEach(function (el) {
if (el._tpx) return;
var t = (el._tpx = build(el));
render(t, true);
if (reduced) { t.blink.pause(); return; }
render(t, false);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-terminal-print>
<p data-cmd>npm install animacious</p>
<p>added 1 package in 0.4s</p>
<p data-cmd>npm run deploy</p>
<p>✓ Live</p>
</div>