GodUIGodUI
90

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.

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.

Chrome, body, line roles

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.

Anatomychrome · 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}

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.

Typing machinecount + typed · 38ms/char

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

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.

LifecycleIO 0.35 · loop 2s · reduced dump

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 restart
window.setTimeout(() => {
  setCount(0);
  setTyped(0);
}, 2000);

// reduced
if (reduced && active) {
  setCount(lines.length);
  setTyped(0);
}

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.

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.

Resultthe real component — types in view, loops after 2s
zsh — godui
$ npx shadcn@latest add @godui/terminal
✔ Installing dependencies.
✔ Created 1 file.
$ pnpm check
✓ All checks passed

On this page

Built with GodUI

Beautifully crafted motion components for modern interfaces.

Star on GitHub