Una librería pensada para diseñadores con prisa
Titular que se reescribe
Un titular grande escribe su final letra a letra, lo selecciona como en un editor y pasa a otra frase.
Ajustes
Código
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 = "Una librería pensada para",
phrases = "diseñadores con prisa, equipos pequeños, tu próximo lanzamiento, gente que cuida el detalle",
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 ? "Reanudar animación" : "Pausar animación"}
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 para IA
Pégalo en ChatGPT, Claude o Cursor y adaptará el bloque a tu proyecto con estos ajustes, aunque no uses React.
Más en Texto y scroll
Dónde usarlo
Un efecto máquina de escribir en React encaja en la portada de una web que habla a varios públicos a la vez: una herramienta «pensada para freelances, equipos pequeños, tu próximo lanzamiento», o un portfolio que dice «diseño para…». El inicio fijo no se mueve mientras el final se escribe, se selecciona y se cambia, como alguien editando de verdad.
Si las frases tienen una longitud parecida, el titular no baila. Con tres a cinco hay de sobra. Tocar el texto lo pausa, cosa que agradece quien quiere leer con calma. Con reducir movimiento activado, cada frase aparece entera sin teclear.