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, 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 stays cold 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 entirely and dumps lines.length in one shot —
caret off, full transcript still 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 only blinks when motion is welcome.
- 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, 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 stays cold 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 entirely and dumps lines.length in one shot —
caret off, full transcript still 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 only blinks when motion is welcome.
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 typewriter.
Motion Score
translatePosition / lift via translate