# React Terminal Component: Typing Animation Without Jank

> Build a React terminal component that types commands, runs spinners and progress bars, prints colored output, never shifts the page and respects reduced motion.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Nov 6, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 8 min
- Canonical: https://wingo-ui.com/blog/react-terminal-component

## TL;DR

A good React terminal component types each command after a prompt, runs a spinner or progress bar, prints colored output, and lets people copy the commands and replay the session. It should lay out the whole transcript from the first frame so the page never shifts, keep the text readable for screen readers and search engines, and show the finished session at once when reduced motion is on. Wingo UI's Terminal (a Pro component) does all of that from one lines array and drives the typing with a single motion value, so React does not re-render once per character.

A React terminal component is the window on a dev-tool landing page where `npm install` types itself, a spinner turns into a check and the deploy URL prints in green. It shows what a CLI does faster than a paragraph can, and it is easy to get wrong: the hero jumps as lines appear, a screen reader hears nothing, the animation replays forever for people who asked for less motion, and a long command pushes the page sideways on a phone. This guide builds a React terminal UI that avoids all four with the [Terminal](https://wingo-ui.com/components/terminal) from Wingo UI. We build that library, so we are not neutral; facts about other projects link to their sources and are dated.

## What should a React terminal component do?

Show a believable session, stay out of the way and stay readable. Here is the list we built against:

- **Typed commands** after a prompt, with a block caret, at a speed you can set.
- **Activity**: a spinner that settles into a check (or a cross when the step failed) and a progress bar that fills to 100%.
- **Toned output**: dimmed by default, with single lines in success, warning or danger colors.
- **Copy and replay** buttons, so nobody retypes a command or reloads the page to watch the session again.
- **No layout shift.** The window has its final height before the first character types.
- **Readable without JavaScript.** The whole transcript is in the server HTML, for crawlers and screen readers alike.
- **Reduced motion** shows the finished session at once.
- **Phone behavior**: long lines scroll inside the window, never the page.

## How do you add a terminal to a React app?

Install it with the CLI. It copies `terminal.tsx` into `components/ui/` along with what it imports (Button, Copy Button, Tooltip and the motion helpers). Terminal is a Pro item, so log in first with `npx wingo-ui@latest login`:

```bash
npx wingo-ui@latest add terminal
```

Then describe the session as data. Each entry in `lines` is one step: a `command` to type, an optional `spinner` or `progress` label that runs for `duration` ms, and `output` lines as strings or `{ text, tone }`.

```tsx
import { Terminal, type TerminalLine } from "@/components/ui/terminal"

const lines: TerminalLine[] = [
  {
    command: "npm install @acme/cli",
    spinner: "Installing @acme/cli",
    duration: 1300,
    output: [{ text: "✓ Added 1 package in 1.9s", tone: "success" }],
  },
  {
    command: "npx acme deploy --prod",
    progress: "Uploading",
    duration: 1600,
    output: [
      "Build    6.1s    214 files",
      "Region   us-east-1 (New York)",
      { text: "✓ Live at https://northwind.acme.app", tone: "success" },
    ],
  },
]

export function DeployHero() {
  return (
    <Terminal
      lines={lines}
      title="emma@macbook: ~/northwind"
      mode="dark"
      trafficLights
      className="max-w-xl"
    />
  )
}
```

Scroll the demo into view to watch it play, then press replay. Try the copy button too: it copies both commands, one per line.

> Live demo (Terminal): Watch the session type itself, then press replay to run it again or copy to grab both commands. Try it at [Terminal](https://wingo-ui.com/components/terminal) and install it with `npx wingo-ui@latest add terminal`.

The props that matter most: `mode="dark"` keeps the window dark on a light page (the classic hero look), `chrome` picks mac dots, a plain title bar or no chrome at all, `tone` or any CSS `color` paints the prompt, caret and progress bars, and `prompt` changes `$` to `❯` or `root@vps:~#`. A step can set its own `prompt` and `delay`. If the copy button should copy something other than the commands, such as a one-line install script, pass it as `copyValue`.

## How do you show a failing step?

Set `tone: "danger"` on the step. The spinner ends in a cross instead of a check, and the output lines can each carry their own tone:

```tsx
const tests: TerminalLine[] = [
  {
    command: "npm test",
    spinner: "Running 48 tests",
    tone: "danger",
    duration: 1500,
    output: [
      { text: "✓ 46 passed", tone: "success" },
      { text: "! 1 skipped   checkout/apple-pay.test.ts", tone: "warning" },
      { text: "✗ 1 failed    cart/totals.test.ts", tone: "danger" },
      "  Expected $128.40, received $128.04",
    ],
  },
]
```

A red line in a marketing terminal sounds odd, but it works well in docs for a "what an error looks like" section, and in a changelog that shows a bug before and after.

## How does a React terminal animation avoid jank?

Keep React out of the per-character loop. The obvious way to build an animated terminal is a `setInterval` that appends one character to state. Magic UI's `TypingAnimation`, for example, starts from an empty string and calls `setDisplayedText` once per character, 60 ms apart by default ([registry source](https://magicui.design/r/terminal.json), checked November 6, 2026). That is fine for one headline. A whole session of commands means a render every 45 to 60 ms for several seconds, and the text is not in the DOM until it has typed.

Wingo UI's Terminal works the other way around. The full transcript is laid out on the first render. One motion value holds the session clock in milliseconds, and every row derives its own state from it: a command reveals itself with a `clip-path` that grows one `ch` at a time, the caret moves with a transform, and output rows switch their opacity at their scheduled time. React renders only when playback starts and ends. Because every row already takes its final space, the window keeps the same height from the first frame to the last, so the content below it never shifts.

Two more rules keep it calm. With `trigger="in-view"` (the default) it waits until about a third of the window is on screen, so a terminal below the fold does not finish before anyone sees it. With `trigger="mount"` it starts as soon as the page has hydrated. With `loop`, it rests while it is off screen instead of burning frames.

## What happens with reduced motion and screen readers?

Under `prefers-reduced-motion: reduce`, the Terminal shows the finished transcript at once and disables the replay button, since there is nothing to replay. It reads the preference through `usePrefersReducedMotion` from `lib/motion`, which is hydration safe; we explain why motion's own hook is not in [Prefers reduced motion in React](https://wingo-ui.com/blog/prefers-reduced-motion-react). `animate={false}` gives the same still result on purpose, for example in a thumbnail or a printed docs page.

For screen readers, the transcript is a focusable region named by the `title` (or `labels.region`, "Terminal" by default). Rows that are not visible yet still contain their text, so the whole session is read in order. Prompts, dots, the caret and the spinner frames are `aria-hidden`, so a reader hears the commands and the output, not "dollar" and braille dots. The copy button announces "Copied" through the [Copy Button](https://wingo-ui.com/components/copy-button) it is built on.

Cult UI made a different, reasonable choice: its Terminal Animation marks the output `role="log"` with `aria-live="polite"`, so new lines are announced as they appear ([docs](https://www.cult-ui.com/docs/components/terminal-animation.md), checked November 6, 2026). That suits output a user triggered. For a hero that plays on its own, we preferred silence over a reader announcing a fake deploy line by line.

## How does the terminal behave on a phone?

Lines never wrap, because a wrapped shell command looks broken and copies badly. Instead the viewport scrolls sideways inside the window, with `overscroll-x-contain` so a swipe does not drag the page, and keyboard users can Tab to it and use the arrow keys. Below 640px the type steps down one size (12px becomes 11px for `size="sm"`, 13px becomes 12px for `md`), which fits a typical `npm install` line on a 390px screen. The copy and replay buttons are regular Button components at the small icon size, which keeps a 32px look and adds a 44px hit area on a coarse pointer.

One pitfall: the typing reveal counts characters in `ch`, the width of one monospace character. Keep emoji and other wide characters out of `command` strings; they are fine in `output`, which does not type.

## How do you control playback from your own button?

Pass `playing` and `onPlayingChange`. With `defaultPlaying={false}` the window waits on an empty prompt until you start it, and `onComplete` fires every time a session finishes:

```tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Terminal, type TerminalLine } from "@/components/ui/terminal"

export function RunDeploy({ lines }: { lines: TerminalLine[] }) {
  const [playing, setPlaying] = useState(false)
  return (
    <div className="flex flex-col gap-3">
      <Terminal
        lines={lines}
        defaultPlaying={false}
        playing={playing}
        onPlayingChange={setPlaying}
        showReplay={false}
      />
      <Button variant="soft" onClick={() => setPlaying((value) => !value)}>
        {playing ? "Pause" : "Run deploy"}
      </Button>
    </div>
  )
}
```

`onPlayingChange(false)` arrives at the end of a session that does not loop, so the button label flips back on its own.

## What terminal components exist for shadcn projects?

shadcn/ui itself has none, so a shadcn terminal component comes from a third-party registry or from npm. What their docs, source and npm pages showed on November 6, 2026:

| Component | Install | Spinner / progress | Copy, replay | Reduced motion |
|---|---|---|---|---|
| [Magic UI Terminal](https://magicui.design/docs/components/terminal) | shadcn registry, `@magicui/terminal` | No | No | Not handled in the source |
| [Cult UI Terminal Animation](https://www.cult-ui.com/docs/components/terminal-animation.md) | shadcn registry, `@cult-ui/terminal-animation` | No | A refresh button in the demo, no copy | Not mentioned in the docs |
| [react-animated-term](https://github.com/dongy7/react-animated-term) | npm, last release 2.0.0 (February 2021) | Spinners through `frames` | No | Not mentioned |
| Wingo UI Terminal | `npx wingo-ui@latest add terminal` | Both | Both | Shows the finished transcript |

Where the others are the better pick: Magic UI and Cult UI are free and MIT licensed, and the Wingo UI Terminal is not. Magic UI composes any JSX as a line, which is handy for custom output. Cult UI has tabbed scenarios built in (npm, pnpm and yarn tabs, say) and a background image option. react-animated-term is a plain npm dependency with no registry, if you can live with a package that has not shipped a release since 2021.

## When is a code block or a text animation the better choice?

When the reader should act on the text. An animated terminal is a demo; the steps of a setup guide belong in a static, highlighted [Code Block](https://wingo-ui.com/components/code-block) with a copy button, which is free:

```tsx
import { CodeBlock } from "@/components/ui/code-block"

export function InstallStep() {
  return <CodeBlock code="npx wingo-ui@latest add terminal" language="bash" filename="Terminal" showLineNumbers={false} />
}
```

> Live demo (Code Block): A highlighted snippet with a filename header and a copy button: select the text or press copy. Try it at [Code Block](https://wingo-ui.com/components/code-block) and install it with `npx wingo-ui@latest add code-block`.

And when you only want a headline to type itself, the full window is too much. [Text Animate](https://wingo-ui.com/components/text-animate) with `effect="typewriter"` types one string with a caret, keeps the element a real `h1` through `as`, and reads the full sentence to screen readers once.

When the output is real, streamed from a build or a server, skip the animation altogether. Our [React log viewer guide](https://wingo-ui.com/blog/react-log-viewer) shows live lines with level filters and a follow mode instead.

## Where does a terminal fit on a landing page?

In the hero, next to the headline, or in a "how it works" band right under it. Keep it to two or three steps; a session that runs longer than about eight seconds loses the reader before the payoff line. Put the real install command in a code block or a copy pill nearby, so nobody has to wait for the animation to copy it. Our [shadcn hero section guide](https://wingo-ui.com/blog/shadcn-hero-section) covers the layouts around it.

If you are building a whole developer platform page, the [Developers Page template](https://wingo-ui.com/templates/developers-page) already has a code hero with language tabs, API surface cards, an endpoint explorer and a CLI terminal that plays deploy and webhook lines when it scrolls into view.

More guides like this one are in the [component guides category](https://wingo-ui.com/blog/category/components), starting with the pillar on [React form components](https://wingo-ui.com/blog/react-form-components-guide).

## Next step

Open the [Terminal docs](https://wingo-ui.com/components/terminal) and try `chrome`, `tone`, `speed` and `loop` in the playground, then install it with `npx wingo-ui@latest add terminal`. Terminal and the Developers Page are part of Wingo UI Pro ([pricing](https://wingo-ui.com/pricing)); the Code Block and Copy Button are free with a free account.

## Components in this post

- [Terminal](https://wingo-ui.com/components/terminal) (Pro): An animated React terminal window for dev-tool landing pages and docs: typed commands, output, spinners and progress bars. Install: `npx wingo-ui@latest add terminal`
- [Code Block](https://wingo-ui.com/components/code-block) (Free): A React code block with syntax highlighting, line numbers, highlighted lines and a copy button, themed from a single accent color. Install: `npx wingo-ui@latest add code-block`
- [Copy Button](https://wingo-ui.com/components/copy-button) (Free): A React copy to clipboard button for IBANs, tax IDs, links and API keys: an icon button with a tooltip, a labelled button and CopyText. Install: `npx wingo-ui@latest add copy-button`
- [Text Animate](https://wingo-ui.com/components/text-animate) (Pro): Animated text for headlines and copy that reveals by word, character or line with a fade, blur, slide, scale or typewriter effect. Install: `npx wingo-ui@latest add text-animate`
- [Developers Page](https://wingo-ui.com/templates/developers-page) (Pro): A React developer platform page template: a code hero with language tabs, API surfaces, a live CLI terminal, endpoints, uptime and limits. Install: `npx wingo-ui@latest add developers-page`

## FAQ

### How do I make an animated terminal in React?

Describe the session as data (each step a command, optional spinner or progress bar, and output lines) and let a component play it on a timeline. With Wingo UI that is npx wingo-ui@latest add terminal and a lines array passed to the Terminal component.

### Does shadcn/ui have a terminal component?

No. As of November 6, 2026, the shadcn/ui component list has no terminal, while Magic UI and Cult UI publish terminal components through the shadcn registry and Wingo UI installs its Terminal as source code through its own CLI.

### Is an animated terminal bad for accessibility?

It can be. Text that builds up one character at a time is announced in fragments or not at all, and motion bothers some readers. Render the full text from the start, reveal it visually only, and skip the animation under prefers-reduced-motion.

### Should I use a terminal or a code block for install commands?

Use a code block when people should read and copy a command, and a terminal when you want to show what happens after they run it. A docs page can use both: the code block in the setup steps, the terminal in the hero.

### Is the Wingo UI Terminal free?

No, Terminal is part of Wingo UI Pro ($8 a month, $80 a year or $150 once). The Code Block and Copy Button it pairs with are free with a free account.

---

Source: https://wingo-ui.com/blog/react-terminal-component
