Skip to content
All Taste components

Text effects

Typewriter

A terminal line that types a command with a blinking caret, shows the result, then starts again.

type · hold · erase · caret · output line

$ npx taste add hero
Added components/hero.tsx
"use client";

/* globals.css
@keyframes caret { to { visibility: hidden; } }
*/
import { useEffect, useState } from "react";

interface TypewriterProps {
  lines: string[];
  /** Milliseconds per character. */
  speed?: number;
}

export function Typewriter({ lines, speed = 55 }: TypewriterProps) {
  const [line, setLine] = useState(0);
  const [count, setCount] = useState(0);
  const [erasing, setErasing] = useState(false);
  const text = lines[line];

  useEffect(() => {
    // Reduced motion: show the whole first line, no typing.
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setCount(text.length);
      return;
    }
    const full = count === text.length;
    const delay = !erasing && full ? 1600 : erasing && count === 0 ? 300 : erasing ? speed / 2 : speed;
    const t = setTimeout(() => {
      if (!erasing && full) setErasing(true);
      else if (erasing && count === 0) {
        setErasing(false);
        setLine((l) => (l + 1) % lines.length);
      } else setCount((c) => c + (erasing ? -1 : 1));
    }, delay);
    return () => clearTimeout(t);
  }, [count, erasing, text, lines.length, speed]);

  return (
    <div className="w-full max-w-sm overflow-hidden rounded-lg border border-border bg-surface font-mono text-[13px] leading-relaxed">
      <div aria-hidden className="flex gap-1.5 border-b border-border px-3 py-2.5">
        <i className="size-2 rounded-full bg-border-strong" />
        <i className="size-2 rounded-full bg-border-strong" />
        <i className="size-2 rounded-full bg-border-strong" />
      </div>
      <p className="px-3.5 py-3 text-fg" aria-label={text}>
        <span aria-hidden className="text-fg-subtle">$ </span>
        <span aria-hidden>{text.slice(0, count)}</span>
        <span aria-hidden className="ml-px inline-block h-[1.15em] w-[7px] translate-y-[3px] bg-primary animate-[caret_1s_steps(2,start)_infinite] motion-reduce:animate-none" />
      </p>
    </div>
  );
}

// <Typewriter lines={["npx taste add hero", "npx taste add pricing"]} />

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.