Code Editor Typing
Code types itself into an editor window, colour-coded as it goes. Line numbers appear as each line starts, indentation snaps in like a real editor’s, and the current line is highlighted. Loop it and the code is selected, cleared and typed again.
import { animate } from 'animejs';
// Spin the logo on load
animate('.logo', {
rotate: '1turn',
scale: [0.8, 1],
duration: 1200,
ease: 'outElastic(1, .6)',
});How to use it
- Paste the snippet just before
</head>. - Add
data-code-editor-typingto a<pre>holding your code. It is built into an editor window and typed out line by line.
Options
Add any of these attributes to the same element.
data-file | The file name on the editor’s tab. Default index.js. |
data-speed | Typing time per character. Default 38 ms. |
data-loop | Select all, clear and type it again after a pause. |
data-hold | How long the finished code stays before looping. Default 2800 ms. |
data-theme="light" | A light editor instead of the dark one. |
The colouring is a small built-in highlighter for JavaScript-style code: keywords, strings, numbers, comments, function calls and object keys. Every line’s space is kept from the start, so the window never grows while it types.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Code Editor Typing: code types itself into an editor window, with syntax
// colours, line numbers, auto-indent and a highlighted current line.
// Usage: <pre data-code-editor-typing>const answer = 42;</pre>
// Optional: data-file (the tab's file name, default "index.js"), data-speed
// (ms per character, default 38), data-loop (select all, clear and type it
// again), data-hold (ms the finished code stays before looping, default
// 2800), data-theme="light" (a light editor).
var DEFAULTS = { file: 'index.js', speed: 38, hold: 2800 };
var KEYWORDS = ('const let var function return if else for while do switch case break continue ' +
'import from export default new await async class extends try catch finally throw typeof ' +
'instanceof in of true false null undefined this yield').split(' ');
var TOKEN = new RegExp(
'(\\/\\/.*)' + // 1 comment
'|(\'(?:[^\'\\\\]|\\\\.)*\'?|"(?:[^"\\\\]|\\\\.)*"?|`(?:[^`\\\\]|\\\\.)*`?)' + // 2 string
'|(\\b\\d+(?:\\.\\d+)?\\b)' + // 3 number
'|\\b(' + KEYWORDS.join('|') + ')\\b' + // 4 keyword
'|([A-Za-z_$][\\w$]*)(?=\\s*\\()' + // 5 function call
'|([A-Za-z_$][\\w$]*)(?=\\s*:)', 'g'); // 6 object key
var KINDS = [null, 'com', 'str', 'num', 'kw', 'fn', 'prop'];
var css =
'.cet{--cet-bg:#13131a;--cet-bar:#1c1c25;--cet-text:#e4e4e7;--cet-ln:#52525b;--cet-line:rgba(255,255,255,.05);' +
'--cet-kw:#c084fc;--cet-str:#86efac;--cet-num:#fdba74;--cet-com:#71717a;--cet-fn:#7dd3fc;--cet-prop:#f9a8d4;--cet-sel:rgba(125,211,252,.22);' +
'margin:0;white-space:normal;text-align:left;background:var(--cet-bg);color:var(--cet-text);border-radius:12px;overflow:hidden;' +
'box-shadow:0 18px 40px rgba(10,10,20,.28);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' +
'.cet-light{--cet-bg:#fff;--cet-bar:#f4f4f5;--cet-text:#27272a;--cet-ln:#a1a1aa;--cet-line:rgba(106,13,173,.06);' +
'--cet-kw:#7c3aed;--cet-str:#15803d;--cet-num:#c2410c;--cet-com:#a1a1aa;--cet-fn:#0369a1;--cet-prop:#be185d;--cet-sel:rgba(106,13,173,.16);' +
'box-shadow:0 18px 40px rgba(106,13,173,.14)}' +
'.cet-bar{display:flex;align-items:center;gap:6px;padding:0 0.9em;background:var(--cet-bar);height:2.5em}' +
'.cet-bar i{width:0.75em;height:0.75em;border-radius:50%;background:#ff5f57;flex:none}' +
'.cet-bar i:nth-child(2){background:#febc2e}.cet-bar i:nth-child(3){background:#28c840}' +
'.cet-tab{margin-left:0.8em;align-self:flex-end;padding:0.45em 0.9em;border-radius:7px 7px 0 0;background:var(--cet-bg);font-size:0.85em;opacity:.85;' +
'white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.cet-body{padding:0.7em 0 0.9em;overflow:hidden}' +
'.cet-line{display:flex;line-height:1.6;min-height:1.6em}' +
'.cet-line.cet-on{background:var(--cet-line)}' +
'.cet-line.cet-sel .cet-code{background:var(--cet-sel)}' +
'.cet-ln{flex:none;width:2.6em;padding-right:1em;text-align:right;color:var(--cet-ln);user-select:none}' +
'.cet-on .cet-ln{color:var(--cet-text)}' +
'.cet-code{white-space:pre;padding-right:1em}' +
'.cet-kw{color:var(--cet-kw)}.cet-str{color:var(--cet-str)}.cet-num{color:var(--cet-num)}' +
'.cet-com{color:var(--cet-com);font-style:italic}.cet-fn{color:var(--cet-fn)}.cet-prop{color:var(--cet-prop)}' +
'.cet-caret{display:inline-block;width:2px;height:1.2em;margin-right:-2px;vertical-align:-0.25em;background:var(--cet-fn)}' +
'.cet-caret.cet-blink{animation:cet-blink 1.06s steps(1) infinite}' +
'@keyframes cet-blink{50%{opacity:0}}' +
'.cet-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 reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
// Strip the blank first/last lines and the indentation the HTML added.
function clean(src) {
var lines = src.replace(/\t/g, ' ').replace(/^\s*\n/, '').replace(/\s+$/, '').split('\n');
var pad = Infinity;
lines.forEach(function (l) { if (l.trim()) pad = Math.min(pad, l.match(/^ */)[0].length); });
return lines.map(function (l) { return l.slice(pad === Infinity ? 0 : pad); });
}
// Split a line into coloured segments: [{t: text, c: kind or ''}].
function tokenise(line) {
var segs = [], last = 0, m;
TOKEN.lastIndex = 0;
while ((m = TOKEN.exec(line))) {
if (!m[0]) { TOKEN.lastIndex++; continue; }
if (m.index > last) segs.push({ t: line.slice(last, m.index), c: '' });
for (var k = 1; k < KINDS.length; k++) if (m[k] !== undefined) break;
segs.push({ t: m[0], c: KINDS[k] });
last = m.index + m[0].length;
}
if (last < line.length) segs.push({ t: line.slice(last), c: '' });
return segs;
}
function html(segs, n) {
var out = '';
for (var i = 0; i < segs.length && n > 0; i++) {
var t = esc(segs[i].t.slice(0, n));
n -= segs[i].t.length;
out += segs[i].c ? '<span class="cet-' + segs[i].c + '">' + t + '</span>' : t;
}
return out;
}
function setup(el) {
var d = el.dataset;
var src = clean(el.textContent);
el.textContent = '';
el.classList.add('cet');
if (d.theme === 'light') el.classList.add('cet-light');
var sr = document.createElement('span');
sr.className = 'cet-sr';
sr.textContent = src.join('\n');
el.appendChild(sr);
var bar = document.createElement('div');
bar.className = 'cet-bar';
bar.setAttribute('aria-hidden', 'true');
bar.innerHTML = '<i></i><i></i><i></i><span class="cet-tab"></span>';
bar.lastChild.textContent = d.file || DEFAULTS.file;
el.appendChild(bar);
var body = document.createElement('div');
body.className = 'cet-body';
body.setAttribute('aria-hidden', 'true');
el.appendChild(body);
var caret = document.createElement('span');
caret.className = 'cet-caret';
var lines = src.map(function (text, i) {
var row = document.createElement('div');
row.className = 'cet-line';
row.innerHTML = '<span class="cet-ln">' + (i + 1) + '</span><span class="cet-code"></span>';
body.appendChild(row);
return { row: row, code: row.lastChild, text: text, segs: tokenise(text) };
});
return { el: el, lines: lines, caret: caret, visible: false, wake: null };
}
// Every line in its finished state (reduced motion, and the end of a run).
function showAll(s) {
s.lines.forEach(function (l) {
l.code.innerHTML = html(l.segs, l.text.length);
l.row.style.visibility = '';
l.row.classList.remove('cet-on', 'cet-sel');
});
}
function whenVisible(s) {
return s.visible ? Promise.resolve() : new Promise(function (r) { s.wake = r; });
}
function typeLine(s, l, speed) {
return new Promise(function (done) {
var indent = l.text.match(/^ */)[0].length; // auto-indent: leading spaces arrive at once
var n = indent;
s.lines.forEach(function (o) { o.row.classList.remove('cet-on'); });
l.row.style.visibility = '';
l.row.classList.add('cet-on');
s.caret.classList.remove('cet-blink');
(function tick() {
l.code.innerHTML = html(l.segs, n);
l.code.appendChild(s.caret);
if (n >= l.text.length) { done(); return; }
var ch = l.text.charAt(n++);
var wait = speed * (0.55 + Math.random() * 0.9);
if (/[{(;,]/.test(ch)) wait += speed * 2;
if (ch === ' ' && Math.random() < 0.15) wait += speed * 3;
setTimeout(tick, wait);
})();
});
}
function play(s) {
var d = s.el.dataset;
var speed = +d.speed || DEFAULTS.speed;
var hold = +d.hold || DEFAULTS.hold;
s.lines.forEach(function (l) {
l.code.textContent = '';
l.row.style.visibility = 'hidden';
l.row.classList.remove('cet-on', 'cet-sel');
});
var chain = sleep(350);
s.lines.forEach(function (l, i) {
chain = chain.then(function () { return typeLine(s, l, speed); })
.then(function () { return sleep(i < s.lines.length - 1 ? speed * 5 : 0); });
});
chain.then(function () {
s.caret.classList.add('cet-blink');
if (d.loop === undefined || d.loop === 'false') return;
sleep(hold).then(function () { return whenVisible(s); }).then(function () {
// Select all, then delete, like a person starting over.
s.lines.forEach(function (l) { if (l.text) l.row.classList.add('cet-sel'); });
return sleep(450);
}).then(function () { play(s); });
});
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._cet;
s.visible = e.isIntersecting;
if (s.visible && s.wake) { var w = s.wake; s.wake = null; w(); }
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-code-editor-typing]').forEach(function (el) {
if (el._cet) return;
var s = (el._cet = setup(el));
showAll(s);
if (reduced()) return;
io.observe(el);
whenVisible(s).then(function () { play(s); });
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><pre data-code-editor-typing data-file="greet.js">
function greet(name) {
return `Hi, ${name}!`;
}
</pre>