# TypingTerminal

> A terminal that types commands and prints their output step by step, for hero sections and docs. Renders the full transcript first, for servers and reduced motion.

Source: https://docs.nasaqui.com/components/typing-terminal

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/typing-terminal.json
```

Shows how a tool is used: it types each command, prints its output line by line, then offers Replay. Output may contain
ANSI colours. An `endSlot` appears at the end for a screenshot of the result or a call to action.

## When to use

- A marketing hero or a docs page that shows an install or a quick start.

## When not to use

- A real, interactive shell: use `Terminal`.
- A command people copy: use a code block with a copy button. The animation is decoration.

## Import

```tsx
import { TypingTerminal } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { TypingTerminal } from "@fadymondy/nasaq/web";

export function Hero() {
  return (
    <TypingTerminal
      title="~/my-shop"
      steps={[
        { cmd: "npx create-togo-app my-shop", out: ["\u001b[32m✓\u001b[0m Installed 214 packages"] },
        { cmd: "cd my-shop && togo dev", out: ["ready on http://localhost:5173"] },
      ]}
    />
  );
}
```

## Anatomy

```
TypingTerminal            data-slot="typing-terminal"  dir="ltr"
├─ header                 dots, title, Replay
├─ transcript             sr-only <pre>, the whole session
└─ body                   data-slot="typing-terminal-body"  fixed height
   ├─ rows                aria-hidden: prompt + command, output lines
   └─ end slot            data-slot="typing-terminal-end"
```

## API

`div` props (except `children` and `title`) plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps` | `{ cmd, out? }[]` | required | Commands without the prompt, and the lines they print. |
| `endSlot` | `ReactNode` | | Shown when the playback ends. |
| `title` | `ReactNode` | `"Terminal"` | In the header. |
| `prompt` | `string` | `"❯"` | |
| `typeMs` / `lineMs` | `number` | `28` / `110` | Speed per character and per line. |
| `loop` | `boolean` | `false` | Start again after 4s instead of showing Replay. |
| `height` | `number` | `320` | Body height in px, so the page does not move. |
| `play` | `boolean` | `true` | Set from an in-view observer to start when scrolled to. `false` shows the full transcript. |
| `onComplete` | `() => void` | | |
| `labels` | `TypingTerminalLabels` | | |

## Accessibility

- Screen readers get the whole transcript once, as text; the animated rows are hidden from them.
- With reduced motion (or under automation) the full transcript shows at once.
- Replay is a real button. The end slot stays reachable.

## RTL & i18n

- The terminal is always left to right, as code is. Labels (title, Replay) are in English and Arabic.

## Styling & tokens

- `bg-nq-surface-soft`, a hairline border and the mono font. ANSI colours map to the chart tokens via `AnsiText`.

## Do / Don't

- Do keep it to two or three commands.
- Don't animate on every scroll. Use `play` once, or `loop` for a hero that stays.

## Related

- [`terminal`](https://docs.nasaqui.com/components/terminal)
- [`marketing-sections`](https://docs.nasaqui.com/components/marketing-sections)

## Lab

https://docs.nasaqui.com/?path=/docs/components-website-typing-terminal--docs

## Code

### React

```tsx
import { TypingTerminal } from "@fadymondy/nasaq/web";

export function Hero() {
  return (
    <TypingTerminal
      title="~/my-shop"
      steps={[
        { cmd: "npx create-togo-app my-shop", out: ["\u001b[32m✓\u001b[0m Installed 214 packages"] },
        { cmd: "cd my-shop && togo dev", out: ["ready on http://localhost:5173"] },
      ]}
    />
  );
}
```

### shadcn

```tsx
import { TypingTerminal } from "@/components/ui/typing-terminal";

export function Hero() {
  return (
    <TypingTerminal
      title="~/my-shop"
      steps={[
        { cmd: "npx create-togo-app my-shop", out: ["\u001b[32m✓\u001b[0m Installed 214 packages"] },
        { cmd: "cd my-shop && togo dev", out: ["ready on http://localhost:5173"] },
      ]}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqTypingTerminal } from "@fadymondy/nasaq/vue";

const steps = [
  { cmd: "npx create-togo-app my-shop", out: ["\u001b[32m✓\u001b[0m Installed 214 packages"] },
  { cmd: "cd my-shop && togo dev", out: ["ready on http://localhost:5173"] },
];
</script>

<template>
  <NqTypingTerminal title="~/my-shop" :steps="steps" />
</template>
```

### Blade

```blade
@php
    $steps = [
        ['cmd' => 'npx create-togo-app my-shop', 'out' => ["\e[32m✓\e[0m Installed 214 packages"]],
        ['cmd' => 'cd my-shop && togo dev', 'out' => ['ready on http://localhost:5173']],
    ];
@endphp
<x-nq::typing-terminal title="~/my-shop" :steps="$steps" />
```

### HTML + Alpine

```html
<div data-slot="typing-terminal" dir="ltr" x-data="nqTypingTerminal(JSON.parse('{\u0022steps\u0022:[{\u0022cmd\u0022:\u0022npx create-togo-app my-shop\u0022,\u0022out\u0022:[\u0022\\u001b[32m\\u2713\\u001b[0m Installed 214 packages\u0022]},{\u0022cmd\u0022:\u0022cd my-shop \\u0026\\u0026 togo dev\u0022,\u0022out\u0022:[\u0022ready on http:\\\/\\\/localhost:5173\u0022]}],\u0022prompt\u0022:\u0022\\u276f\u0022,\u0022typeMs\u0022:28,\u0022lineMs\u0022:110,\u0022loop\u0022:false,\u0022play\u0022:true}'))" x-bind:data-playing="playing ? '' : null"
    class="relative flex min-w-0 flex-col overflow-hidden rounded-surface border border-border bg-nq-surface-soft text-start">
    <div data-slot="typing-terminal-header" class="flex h-row shrink-0 items-center gap-2 border-b border-border ps-3 pe-1.5">
        <span aria-hidden="true" class="flex gap-1.5">
            <span class="size-2.5 rounded-full bg-border"></span>
            <span class="size-2.5 rounded-full bg-border"></span>
            <span class="size-2.5 rounded-full bg-border"></span>
        </span>
        <span class="min-w-0 flex-1 truncate font-mono text-caption text-muted-foreground">~/my-shop</span>
        <button data-slot="button"
     type="button"                         style="display: none" x-show="showReplay" x-on:click="replay()" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control-sm px-2.5">
        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
  <path d="M3 3v5h5"/>
</svg>            Replay</button>
    </div>
    <pre class="sr-only" aria-label="Terminal session">❯ npx create-togo-app my-shop
✓ Installed 214 packages
❯ cd my-shop &amp;&amp; togo dev
ready on http://localhost:5173</pre>
    <div x-ref="body" data-slot="typing-terminal-body" style="height: 320px" class="min-h-0 overflow-auto px-4 py-3 font-mono text-code leading-relaxed">
        <div aria-hidden="true">
            <div x-ref="ssr">
                                    <div data-kind="command" class="whitespace-pre-wrap break-words"><span class="text-primary">❯ </span><span class="text-foreground">npx create-togo-app my-shop</span></div>
                                            <div data-kind="output" class="whitespace-pre-wrap break-words text-muted-foreground"><span class="block min-h-[1lh]">✓ Installed 214 packages</span></div>
                                                        <div data-kind="command" class="whitespace-pre-wrap break-words"><span class="text-primary">❯ </span><span class="text-foreground">cd my-shop &amp;&amp; togo dev</span></div>
                                            <div data-kind="output" class="whitespace-pre-wrap break-words text-muted-foreground"><span class="block min-h-[1lh]">ready on http://localhost:5173</span></div>
                                                </div>
            <template x-for="row in rows" :key="row.key">
                <div x-bind:data-kind="row.kind" class="whitespace-pre-wrap break-words" x-bind:class="row.kind === 'output' ? 'text-muted-foreground' : ''">
                    <span x-show="row.kind === 'command'" class="text-primary" x-text="prompt + ' '"></span>
                    <span x-show="row.kind === 'command'" class="text-foreground" x-text="row.cmd"></span>
                    <span x-show="row.typing" style="display: none" aria-hidden="true" class="ms-0.5 inline-block h-[1.1em] w-[0.55em] translate-y-[0.15em] animate-pulse bg-primary"></span>
                    <template x-for="(spans, r) in row.rows" :key="r">
                        <span class="block min-h-[1lh]">
                            <template x-for="(s, j) in spans" :key="j">
                                <span x-bind:style="spanStyle(s.style)" x-bind:class="spanClass(s.style)" x-text="s.text"></span>
                            </template>
                        </span>
                    </template>
                </div>
            </template>
        </div>
            </div>
</div>
```
