Studio KitAdvanced Text Animationsmotion/react

Typewriter

Types and deletes a rotating list of words after a static prefix, with configurable typing speed, hold pause, accent color, and a blinking caret.

Preview

Build interfaces, landing pages, dashboards, design systems

Customize

6 props
70
1400

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add typewriter

Writes components/ui/typewriter.tsx and components/styles/typewriter.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { Typewriter } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <Typewriter />6  );7}

Props

PropTypeDefault
words

Words typed and deleted in sequence.

string[]['interfaces', 'landing pages', 'dashboards', 'design systems']
prefix

Static text rendered before the typed word.

string'Build'
speed

Milliseconds per typed character; deleting runs twice as fast.

number70
pause

Milliseconds to hold a completed word before deleting.

number1400
cursor

Show the blinking caret.

booleantrue
accentColor

Color of the typed word and caret.

string'#38bdf8'
className

Additional class merged onto the root element.

string''

Accessibility

A visually hidden span announces the prefix and the full word list once; the character-by-character output and caret are aria-hidden so screen readers are not spammed. Under prefers-reduced-motion the first word is shown statically with a steady caret.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/typewriter.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/typewriter.module.css";6 7export interface TypewriterProps {8  /** Words typed and deleted in sequence */9  words?: string[];10  /** Static text rendered before the typed word */11  prefix?: string;12  /** Milliseconds per typed character (deleting runs at twice the speed) */13  speed?: number;14  /** Milliseconds to hold a completed word before deleting */15  pause?: number;16  /** Show the blinking cursor */17  cursor?: boolean;18  /** Color of the typed word and cursor */19  accentColor?: string;20  /** Optional className */21  className?: string;22}23 24const DEFAULT_WORDS = ["interfaces", "landing pages", "dashboards", "design systems"];25 26export function Typewriter({27  words = DEFAULT_WORDS,28  prefix = "Build",29  speed = 70,30  pause = 1400,31  cursor = true,32  accentColor = "#38bdf8",33  className = "",34}: TypewriterProps) {35  const prefersReducedMotion = useReducedMotion();36  const list = words.length > 0 ? words : DEFAULT_WORDS;37  const [index, setIndex] = React.useState(0);38  const [text, setText] = React.useState("");39  const [deleting, setDeleting] = React.useState(false);40  const word = list[index % list.length] ?? "";41 42  React.useEffect(() => {43    if (prefersReducedMotion) return;44    let t: ReturnType<typeof setTimeout> | undefined;45    if (!deleting && text === word) {46      t = setTimeout(() => setDeleting(true), pause);47    } else if (deleting && text === "") {48      setDeleting(false);49      setIndex((i) => (i + 1) % list.length);50    } else {51      t = setTimeout(52        () => setText(deleting ? word.slice(0, text.length - 1) : word.slice(0, text.length + 1)),53        deleting ? speed / 2 : speed,54      );55    }56    return () => {57      if (t) clearTimeout(t);58    };59  }, [text, deleting, word, list.length, speed, pause, prefersReducedMotion]);60 61  const shown = prefersReducedMotion ? list[0] : text;62  const label = `${prefix ? `${prefix} ` : ""}${list.join(", ")}`;63 64  return (65    <span66      style={{ "--mf-tw-accent": accentColor } as React.CSSProperties}67      className={`${styles.root} ${className}`.trim()}68    >69      <span className={styles.srOnly}>{label}</span>70      <span aria-hidden="true">71        {prefix ? <span className={styles.prefix}>{prefix} </span> : null}72        <span className={styles.typed}>{shown}</span>73        {cursor ? (74          <motion.span75            className={styles.cursor}76            animate={prefersReducedMotion ? { opacity: 1 } : { opacity: [1, 0] }}77            transition={78              prefersReducedMotion ? undefined : { duration: 0.6, repeat: Infinity, repeatType: "reverse" }79            }80          />81        ) : null}82      </span>83    </span>84  );85}