← Typing

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

  1. Paste the snippet just before </head>.
  2. Add data-terminal-print to a container with one element per line. Mark commands with data-cmd; every other line is output.

Options

Add any of these attributes to the same element.

data-promptThe prompt before each command. Default $.
data-speedTyping time per character. Default 45 ms.
data-titleText in the window’s title bar. Default “Terminal”.
data-chrome="false"Hide the window frame and its three dots.
data-loopClear and replay after a pause.

Every line’s space is reserved up front, so the box never grows while it plays.

1 · Snippet
<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>
2 · Your HTML
<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>