A library made for designers in a hurry

React + MotionText & scroll

Rewriting headline

A big headline types its ending letter by letter, selects it like a text editor and swaps in the next phrase.

Settings

65ms

Code

import { useEffect, useMemo, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";

interface TypewriterRotatorProps {
  prefix?: string;
  /** Frases separadas por comas. */
  phrases?: string;
  /** Milisegundos por letra al escribir. */
  speed?: number;
  caret?: string;
}

export default function TypewriterRotator({
  prefix = "A library made for",
  phrases = "designers in a hurry, small teams, your next launch, people who care about detail",
  speed = 65,
  caret = "#7c5cff",
}: TypewriterRotatorProps) {
  const list = useMemo(
    () =>
      phrases
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean),
    [phrases],
  );
  const [index, setIndex] = useState(0);
  const [length, setLength] = useState(() => list[0]?.length ?? 0);
  const [selecting, setSelecting] = useState(false);
  const [paused, setPaused] = useState(false);
  const reduce = useReducedMotion();
  const word = list.length ? (list[index % list.length] ?? "") : "";

  useEffect(() => {
    if (paused || !word) return;
    let delay: number;
    let next: () => void;
    if (reduce) {
      delay = 2600;
      next = () => setIndex((i) => i + 1);
    } else if (selecting) {
      delay = 480;
      next = () => {
        setSelecting(false);
        setLength(0);
        setIndex((i) => i + 1);
      };
    } else if (length < word.length) {
      delay = length === 0 ? 320 : speed * (0.6 + Math.random() * 0.8);
      next = () => setLength(length + 1);
    } else {
      delay = 1900;
      next = () => setSelecting(true);
    }
    const t = setTimeout(next, delay);
    return () => clearTimeout(t);
  }, [paused, word, reduce, selecting, length, speed]);

  const shown = reduce ? word : word.slice(0, length);
  const typing = !paused && !reduce && !selecting && length < word.length;

  return (
    <div className="w-full max-w-md">
      <p className="sr-only">
        {prefix} {word}
      </p>
      <button
        type="button"
        onClick={() => setPaused((p) => !p)}
        aria-label={paused ? "Resume animation" : "Pause animation"}
        aria-pressed={paused}
        className="block w-full cursor-pointer text-left text-[34px] font-semibold leading-[1.06] tracking-[-0.035em] sm:text-[42px]"
      >
        <span aria-hidden="true" className="block text-balance text-[#1d1d1f] dark:text-[#f5f5f7]">
          {prefix}
        </span>
        <span aria-hidden="true" className="block min-h-[2.12em]" style={{ color: caret }}>
          <span
            className="rounded-[0.1em] transition-colors duration-150"
            style={{
              background: selecting ? "color-mix(in srgb, " + caret + " 22%, transparent)" : "transparent",
            }}
          >
            {shown}
          </span>
          <motion.span
            className="ml-[0.05em] inline-block h-[0.86em] w-[0.075em] translate-y-[0.12em] rounded-full"
            style={{ background: caret }}
            animate={{ opacity: paused ? 0.3 : typing ? 1 : [1, 1, 0, 0] }}
            transition={
              typing || paused
                ? { duration: 0.15 }
                : { duration: 1, repeat: Infinity, times: [0, 0.5, 0.5, 1] }
            }
          />
        </span>
      </button>
    </div>
  );
}

Prompt for AI

Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.

More in Text & scroll

Where to use it

A typewriter effect in React fits the hero of a page that speaks to several audiences at once: a design tool “made for freelancers, small teams, your next launch”, or a portfolio that says “I design for…”. The fixed start stays put while the ending is typed, highlighted and replaced, which reads more like a person editing than a gimmick.

Phrases of similar length stop the headline from jumping around. Three to five is plenty. Tapping the text pauses it, which helps anyone who wants to read calmly. With reduced motion switched on, each phrase simply appears whole.