# Typewriter text

Terminal typing with uneven keystrokes, a block, underscore or bar cursor, an optional prompt and backspacing through a list of phrases.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/typewriter-text.json](https://asciicn.dev/r/typewriter-text.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://asciicn.dev/r/typewriter-text.json
```

## Usage [#usage]

```tsx
import { TypewriterText } from "@/components/asciicn/typewriter-text";

export function Install() {
  return (
    <TypewriterText
      as="p"
      prompt="$ "
      phrases={["pnpm add asciicn", "pnpm dev"]}
      className="font-mono"
    />
  );
}
```

Characters land one at a time, `speed` per second on average. `jitter` makes the gaps uneven: a little longer after a space, longer still after punctuation, and now and then a short hesitation. Each finished phrase holds for `pause` seconds, then backspaces at `backspaceSpeed` and the next one starts. After the last phrase it starts over, unless `loop` is `false`.

The cursor is a glyph from the glyph font: `█`, `_` or `▏`. It stays solid while keys land and blinks in steps while it waits, as a terminal cursor does.

## Examples [#examples]

### Rotating headline [#rotating-headline]

One `TypewriterText` on the second line of a heading, cycling phrases behind an orange underscore cursor. The element takes the size of its longest phrase, so the heading does not reflow while it types.

```tsx title="examples/typewriter-text-hero.tsx"
import { TypewriterText } from "@/components/asciicn/typewriter-text";

export default function TypewriterTextHero() {
  return (
    <h3 className="font-display w-full max-w-2xl text-4xl leading-tight font-bold tracking-tight sm:text-6xl">
      Interfaces that
      <br />
      <TypewriterText
        phrases={["render in glyphs", "boot like a terminal", "ship today"]}
        cursor="underscore"
        speed={12}
        pause={2.2}
        cursorColor="orange"
      />
    </h3>
  );
}
```

### Boot log [#boot-log]

Three log lines with `loop={false}` and staggered `delay`s, then a ready line. `showCursor="typing"` shows each log line's cursor only while it types. The ready line uses `"started"`, so its cursor appears with its first key and keeps blinking.

```tsx title="examples/typewriter-text-boot.tsx"
import { TypewriterText } from "@/components/asciicn/typewriter-text";

const LINES = [
  { delay: 0.2, text: "BIOS 4.02 · memory check 65536K ok" },
  { delay: 1.8, text: "mounting /dev/glyph0 on /usr/share/ascii" },
  { delay: 3.8, text: "starting asciicn renderer ... done" },
] as const;

export default function TypewriterTextBoot() {
  return (
    <div className="flex w-full max-w-xl flex-col gap-1 font-mono text-xs text-muted-foreground sm:text-sm">
      {LINES.map((line) => (
        <TypewriterText
          key={line.text}
          as="p"
          phrases={[line.text]}
          loop={false}
          speed={34}
          jitter={0.3}
          delay={line.delay}
          cursor="bar"
          showCursor="typing"
        />
      ))}
      <TypewriterText
        as="p"
        phrases={["ready."]}
        loop={false}
        delay={5.4}
        speed={10}
        showCursor="started"
        className="text-foreground"
      />
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `phrases` | `readonly string[]` |  | The lines to type, in order. Screen readers get all of them. |
| `as` | `"h1" \| "h2" \| "h3" \| "h4" \| "p" \| "span" \| "div"` | `"span"` | The element to render. |
| `speed` | `number` | `16` | Characters typed per second, on average. |
| `backspaceSpeed` | `number` | `40` | Characters erased per second when backspacing. |
| `jitter` | `number` | `0.6` | How uneven the typing is, from 0 (even) to 1. |
| `pause` | `number` | `1.8` | Seconds to hold a typed phrase before erasing it. |
| `delay` | `number` | `0.2` | Seconds to wait before the first key. |
| `backspace` | `boolean` | `true` | Erase phrases one character at a time. false clears the line at once. |
| `loop` | `boolean` | `true` | Start over after the last phrase. false stops on the last phrase. |
| `cursor` | `"block" \| "underscore" \| "bar"` | `"block"` | Cursor shape: █, _ or ▏. |
| `showCursor` | `"always" \| "started" \| "typing"` | `"always"` | When the cursor shows: always, from the first key on, or only while keys land. |
| `prompt` | `string` |  | Text before the typed line that is never erased, such as "$ ". |
| `cursorColor` | `AsciiColor` | `"ink"` | Cursor colour. |
| `promptColor` | `AsciiColor` | `"grey"` | Prompt colour. |

Other props pass through to the element.

## Notes [#notes]

* Every phrase is laid out invisibly in the same grid cell as the typed line, so the element always takes the size of its longest phrase. Typing and erasing never move the text around it.
* Screen readers get every phrase once, from a visually hidden copy. The typed line is `aria-hidden`, so a screen reader does not read each keystroke.
* With reduced motion nothing types and the cursor does not blink. The text shows in full: the first phrase when `loop` is on, the last phrase when it is off.
* Typing stops while the text is offscreen or the tab is hidden, and carries on from the same character when it comes back.
* Changing `phrases` starts over from the first phrase.