Une bibliothèque pensée pour les designers pressés
Titre qui se réécrit
Un grand titre tape sa fin lettre par lettre, la sélectionne comme dans un éditeur puis change de phrase.
Réglages
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 = "Une bibliothèque pensée pour",
phrases = "les designers pressés, les petites équipes, votre prochain lancement, les gens attentifs au détail",
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 ? "Reprendre l’animation" : "Mettre l’animation en pause"}
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 pour l’IA
Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.
Plus dans Texte et défilement
Où l’utiliser
Un effet machine à écrire en React convient à l’en-tête d’un site qui parle à plusieurs publics : un outil « pensé pour les freelances, les petites équipes, votre prochain lancement », ou un portfolio qui annonce « je conçois pour… ». Le début reste fixe pendant que la fin s’écrit, se sélectionne et change, comme une vraie retouche.
Des phrases de longueur proche évitent que le titre ne saute. Trois à cinq suffisent largement. Un toucher sur le texte le met en pause, ce qu’apprécient ceux qui veulent lire tranquillement. Avec l’option de réduction des animations, chaque phrase apparaît entière.