Typewriter Status Log Loader — Character-by-Character Build Log in HTML CSS JS
Typewriter Status Log Loader · Loaders · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="tl-console">
<div class="tl-titlebar"><span class="tl-dot r"></span><span class="tl-dot y"></span><span class="tl-dot g"></span><span class="tl-title">build.log</span></div>
<div class="tl-log" id="tlLog"></div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b0f19;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.tl-console{width:100%;max-width:420px;background:#0d1117;border:1px solid #21262d;border-radius:12px;overflow:hidden;box-shadow:0 20px 50px rgba(0,0,0,.5)}
.tl-titlebar{display:flex;align-items:center;gap:6px;padding:10px 14px;background:#161b22;border-bottom:1px solid #21262d}
.tl-dot{width:10px;height:10px;border-radius:50%}
.tl-dot.r{background:#f87171}.tl-dot.y{background:#fbbf24}.tl-dot.g{background:#34d399}
.tl-title{margin-left:6px;font-size:11.5px;color:#7d8590;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.tl-log{padding:16px 16px 18px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;line-height:1.85;min-height:190px}
.tl-line{white-space:pre;color:#8b949e}
.tl-line .tl-ok{color:#3fb950}
.tl-line .tl-tag{color:#58a6ff}
.tl-cursor{display:inline-block;width:7px;height:13px;background:#58a6ff;vertical-align:-2px;animation:tlBlink 1s step-end infinite}
@keyframes tlBlink{50%{opacity:0}}var LOG = [
{ text: '$ npm run build', tag: true },
{ text: 'Compiling assets...' },
{ text: 'Optimizing images...' },
{ text: 'Bundling modules (214 files)...' },
{ text: 'Purging unused CSS...' },
{ text: 'Generating source maps...' },
{ text: 'Done in 3.42s.', ok: true },
];
var logEl = document.getElementById('tlLog');
var CHAR_MS = 18; // real per-character typing speed
var LINE_GAP_MS = 260; // pause between finishing one line and starting the next
function typeLine(entry, onDone) {
var row = document.createElement('div');
row.className = 'tl-line';
logEl.appendChild(row);
var textSpan = document.createElement('span');
if (entry.ok) textSpan.className = 'tl-ok';
else if (entry.tag) textSpan.className = 'tl-tag';
row.appendChild(textSpan);
var cursor = document.createElement('span');
cursor.className = 'tl-cursor';
row.appendChild(cursor);
var i = 0;
var full = entry.text;
// Genuine character-by-character reveal via a real interval — not a CSS
// width/steps() typewriter trick, so line length and content are fully
// dynamic and each character's timing is a real scheduled tick.
var timer = setInterval(function () {
i++;
textSpan.textContent = full.slice(0, i);
if (i >= full.length) {
clearInterval(timer);
cursor.remove();
setTimeout(onDone, LINE_GAP_MS);
}
}, CHAR_MS);
}
function runLog(index) {
if (index >= LOG.length) return;
typeLine(LOG[index], function () { runLog(index + 1); });
}
runLog(0);Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Typewriter Status Log Loader — A Build-Console Log That Types Itself Out

Some loading states are best communicated as a literal log — the terminal-style scroll of "Compiling assets…", "Optimizing images…", "Done." that CI tools and build systems show while they work. This snippet renders exactly that: a mock console window where each status line reveals itself character-by-character before the next line begins, using a real per-character interval rather than a CSS fade or a steps()-based faux-typewriter trick.
Real character-by-character typing
typeLine() uses a genuine setInterval ticking every CHAR_MS (18ms) that increments an index i and sets textSpan.textContent = full.slice(0, i) — each tick reveals exactly one more character of the actual string. This is meaningfully different from the common CSS steps() typewriter trick, which only works for a fixed-width, fixed-content <span> known at build time; because this is driven by a real loop reading real string data, log lines can be any length, generated dynamically, or even streamed in from a server one line at a time.
One line finishes before the next starts
runLog(index) types the current entry, and only once its interval clears (the full string has been revealed) does it wait LINE_GAP_MS and then recursively call runLog(index + 1) for the next line — a genuine sequential reveal, not several lines fading in on staggered but independent timers. This mirrors how a real build log actually streams: each line completes before the next begins.
Distinct from a "thinking" indicator
This is deliberately literal and technical — monospace font, a mock console titlebar with the classic red/yellow/green window dots, a blinking block cursor, and syntax-style coloring (a blue command prompt, green success line) — the register of a CI/deploy log, not an AI "thinking…" indicator like the shimmering, vague status text of an ai thinking loader. The messages here are concrete completed actions ("Bundling modules (214 files)…"), not open-ended reasoning narration.
Data-driven and extensible
The whole log comes from one LOG array of { text, tag?, ok? } entries, so changing the script is editing a list — swap in your real build/deploy/import steps, or drive entries in dynamically as a long-running task actually completes each stage. Pair it with a top loading bar for an overall progress cue alongside the detailed log, or a loading overlay as the surrounding shell.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA mock console window renders and the first log line begins typing.
- 2Watch each line type outCharacters appear one at a time with a blinking cursor, then the next line begins.
- 3Reach the final lineThe last line types out in green, signaling completion.
- 4Edit the LOG arrayChange, add, or remove { text, tag, ok } entries for your real process's steps.
- 5Tune the typing speedAdjust CHAR_MS (per-character delay) and LINE_GAP_MS (between-line pause).
- 6Drive it from real eventsCall typeLine() for each step as your actual task completes it, instead of a fixed array.
Real-world uses
Common Use Cases
setInterval that reveals one character per tick.Got questions?
Frequently Asked Questions
A CSS steps() typewriter animates a fixed-width element's clip or width in discrete steps, which only works cleanly for a single line of known, fixed-length text set at build time. This snippet instead uses a real setInterval that reveals one character of an actual string per tick, so lines can be any length, generated dynamically, added to an array at runtime, or even streamed in from a live source — CSS steps() can't do any of that.
runLog(index) only calls itself for the next index inside typeLine's onDone callback, which fires after the interval has revealed every character and a short LINE_GAP_MS pause has elapsed. This produces a genuinely sequential reveal — one line completing triggers the next — matching how a real build log actually streams output, rather than several independent fade-in timers that might overlap.
CHAR_MS controls the delay between each revealed character (lower is faster typing); LINE_GAP_MS controls the pause after a line finishes before the next one starts. Both are plain constants at the top of the script.
Call typeLine({ text: 'Your real status message' }, callback) directly from your task's own progress events as each stage actually completes, instead of iterating a pre-built LOG array — the typing mechanism doesn't care where the text comes from, only that you provide a string and a callback for when it should type.
Keep the setInterval-based character reveal logic in a small hook/composable that takes a string and a speed, returning the currently-revealed substring as state, updated on each tick with cleanup on unmount. Drive a list of these hook instances sequentially (starting the next only once the current one's revealed length equals its full text length) to reproduce the same one-line-at-a-time behavior declaratively.