Anatomy of Terminal
How a count/typed state machine types commands character-by-character, gates playback with IntersectionObserver, and loops after a two-second pause.
↓Scroll to step through it
Chrome, body, line roles
Terminal is a small state machine dressed as a window. Commands type
out one character at a time; output and comment lines appear after a
beat. An optional chrome bar with traffic lights frames the mono body,
and a pulsing caret marks the active line.
showChrome (default true) renders the header: three dots, an optional
centered title, and a balancing spacer. The body is
font-mono text-[13px] with ligatures off so glyphs don't shift as they
type. Each line is command, output, or comment, each with its own
foreground opacity.
The typing state machine
Two pieces of state drive the script: count (fully finished lines) and
typed (characters revealed on the current command). While
typed < line.text.length on a command, a timeout bumps typed every
typingSpeed ms (default 38). Then a pause (320 after a command,
line.delay ?? 380 otherwise) advances count and resets typed.
The caret is a primary-colored block with animate-pulse, shown on the
active command, or on the last line when the script is done and
loop is false.
Start on view, loop, reduce
Playback waits until the terminal crosses an
IntersectionObserver at threshold: 0.35 (when startOnView is
true). Once active, finishing the script with loop waits 2s, then
resets count and typed to zero. Under prefers-reduced-motion, the
machine skips typing and renders all lines.length lines at once. The
caret is off, and the full transcript stays in sr-only for assistive tech.
The result
A count/typed machine, a 0.35 IO gate, a 2s loop pause, and a caret that blinks only when reduced motion is off.
- Chrome
- showChrome: traffic lights + optional title
- Body
- mono 13px, space-y-1, ligatures off
- Lines
- command · output · comment color roles
{showChrome ? (<div className={HEADER_BASE}> <div className="flex gap-1.5" aria-hidden> <span className="size-3 rounded-full bg-red-500/80" /> <span className="size-3 rounded-full bg-yellow-500/80" /> <span className="size-3 rounded-full bg-green-500/80" /> </div> {title ? <span className="…">{title}</span> : null}</div>) : null}Anatomy of Terminal
How a count/typed state machine types commands character-by-character, gates playback with IntersectionObserver, and loops after a two-second pause.
- Chrome
- showChrome: traffic lights + optional title
- Body
- mono 13px, space-y-1, ligatures off
- Lines
- command · output · comment color roles
{showChrome ? (<div className={HEADER_BASE}> <div className="flex gap-1.5" aria-hidden> <span className="size-3 rounded-full bg-red-500/80" /> <span className="size-3 rounded-full bg-yellow-500/80" /> <span className="size-3 rounded-full bg-green-500/80" /> </div> {title ? <span className="…">{title}</span> : null}</div>) : null}Chrome, body, line roles
Terminal is a small state machine dressed as a window. Commands type
out one character at a time; output and comment lines appear after a
beat. An optional chrome bar with traffic lights frames the mono body,
and a pulsing caret marks the active line.
showChrome (default true) renders the header: three dots, an optional
centered title, and a balancing spacer. The body is
font-mono text-[13px] with ligatures off so glyphs don't shift as they
type. Each line is command, output, or comment, each with its own
foreground opacity.
setTyped(t => t + 1) · then setCount(c => c + 1)
- Command
- typed++ every typingSpeed ms (38)
- Output
- lands after delay (default 380ms)
- Caret
- animate-pulse on the active command
if (isCommand && typed < line.text.length) {const id = window.setTimeout(() => setTyped((t) => t + 1), typingSpeed);return () => window.clearTimeout(id);}const pause = isCommand ? 320 : (line.delay ?? 380);const id = window.setTimeout(() => {setCount((c) => c + 1);setTyped(0);}, pause);The typing state machine
Two pieces of state drive the script: count (fully finished lines) and
typed (characters revealed on the current command). While
typed < line.text.length on a command, a timeout bumps typed every
typingSpeed ms (default 38). Then a pause (320 after a command,
line.delay ?? 380 otherwise) advances count and resets typed.
The caret is a primary-colored block with animate-pulse, shown on the
active command, or on the last line when the script is done and
loop is false.
viewport · threshold 0.35
- startOnView
- IO threshold 0.35 → setActive(true), disconnect
- Loop
- 2s pause, then count=0 / typed=0
- Reduced
- dump lines.length immediately, no caret
const io = new IntersectionObserver((entries) => { if (entries.some((e) => e.isIntersecting)) { setActive(true); io.disconnect(); }},{ threshold: 0.35 },);// loop restartwindow.setTimeout(() => {setCount(0);setTyped(0);}, 2000);// reducedif (reduced && active) {setCount(lines.length);setTyped(0);}Start on view, loop, reduce
Playback waits until the terminal crosses an
IntersectionObserver at threshold: 0.35 (when startOnView is
true). Once active, finishing the script with loop waits 2s, then
resets count and typed to zero. Under prefers-reduced-motion, the
machine skips typing and renders all lines.length lines at once. The
caret is off, and the full transcript stays in sr-only for assistive tech.
The result
A count/typed machine, a 0.35 IO gate, a 2s loop pause, and a caret that blinks only when reduced motion is off.
Accessibility
The animated view is aria-hidden. A parallel sr-only block always
exposes the full script (prompt + text for commands, plain text for
the rest), so screen readers never wait on the typing animation.
Motion Score
translatePosition / lift via translate