niceui

Typewriter New

Types, holds and deletes rotating phrases behind a blinking caret.

ReactTailwind CSSTypeScriptEffectsMIT · Free
Live preview

Installation

01

Install with the shadcn CLI. The NiceUI theme comes along the first time.

$ npx shadcn@latest add https://niceui.dev/r/effects-typewriter-text.json
02

Or copy the source below. Dependencies: none.

Source

components/niceui/effects/TypewriterText.tsx
import { useEffect, useState } from 'react';
import { cn } from '@/lib/cn';

export type TypewriterTextProps = { phrases: string[]; typeMs?: number; deleteMs?: number; holdMs?: number; className?: string };

/** Types each phrase, holds, deletes it, then moves to the next. */
export function TypewriterText({ phrases, typeMs = 60, deleteMs = 32, holdMs = 1400, className }: TypewriterTextProps) {
  const [i, setI] = useState(0);
  const [n, setN] = useState(0);
  const [del, setDel] = useState(false);
  const word = phrases[i % phrases.length];

  useEffect(() => {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setN(word.length);
      return;
    }
    let t: number;
    if (!del && n < word.length) t = window.setTimeout(() => setN(n + 1), typeMs);
    else if (!del) t = window.setTimeout(() => setDel(true), holdMs);
    else if (n > 0) t = window.setTimeout(() => setN(n - 1), deleteMs);
    else t = window.setTimeout(() => (setDel(false), setI((x) => x + 1)), 250);
    return () => clearTimeout(t);
  }, [n, del, word, typeMs, deleteMs, holdMs]);

  return (
    <span className={cn('inline-flex items-baseline', className)} aria-label={word}>
      <span aria-hidden>{word.slice(0, n)}</span>
      <span aria-hidden className="ml-0.5 inline-block h-[0.9em] w-[3px] translate-y-[0.1em] animate-blink rounded-full bg-brand-400" />
    </span>
  );
}

export function TypewriterTextDemo() {
  return (
    <h2 className="text-center text-3xl font-semibold tracking-[-0.03em] text-ink-50 sm:text-5xl">
      Build interfaces that
      <br />
      <TypewriterText className="font-serif font-normal italic text-brand-300" phrases={['feel inevitable.', 'ship on Friday.', 'agents can use.', 'people remember.']} />
    </h2>
  );
}