MynduiMyndui
0

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.

01/04Chrome, body, line roles
Chrome
showChrome — traffic lights + optional title
Body
mono 13px, space-y-1, ligatures off
Lines
command · output · comment color roles
tsx
{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}
01

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.

02/04The typing state machine

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
tsx
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);
02

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.

03/04Start on view, loop, reduce

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
tsx
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);}
03

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.

04/04Result
zsh — myndui
$ npx shadcn@latest add @myndui/terminal
✔ Installing dependencies.
✔ Created 1 file.
$ pnpm check
✓ All checks passed
04

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

TerminalSSCompositor-only
StranslatePosition / lift via translate
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →