Toca el titular para descifrarlo otra vez
Titre qui se déchiffre
Les lettres d’un titre commencent en bruit puis se fixent une à une, sans décaler la mise en page.
Réglages
Code
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { useInView, useReducedMotion } from "framer-motion";
import { RotateCcw } from "lucide-react";
interface ScrambleTextProps {
text?: string;
color?: string;
/** Milisegundos hasta que se descifra entero. */
duration?: number;
}
const NARROW = "il1!|:;/";
const UPPER = "ABDEFHKLNPRSTUXZ";
const LOWER = "abcdeghknopqsuvxyz#*";
const glyphSet = (c: string) => (/[ilíjtfr.,;:!¡'|]/.test(c) ? NARROW : /[A-ZÁÉÍÓÚÑ]/.test(c) ? UPPER : LOWER);
const glyph = (c: string, n: number) => {
const set = glyphSet(c);
return set.charAt(Math.floor(n * set.length) % set.length);
};
const noise = (text: string) => [...text].map((c, i) => (c === " " ? null : glyph(c, ((i * 7 + 3) % 10) / 10)));
export default function ScrambleText({
text = "Moins de bruit. Plus de signal.",
color = "#ff375f",
duration = 1600,
}: ScrambleTextProps) {
const ref = useRef<HTMLButtonElement>(null);
const seen = useInView(ref, { once: true, amount: 0.5 });
const reduce = useReducedMotion();
const raf = useRef(0);
const [glyphs, setGlyphs] = useState<(string | null)[]>(() => noise(text));
const run = useCallback(() => {
cancelAnimationFrame(raf.current);
const chars = [...text];
const locks = chars.map((_, i) => (i / chars.length) * 0.75 + Math.random() * 0.25);
const start = performance.now();
let last = 0;
const step = (now: number) => {
const p = reduce ? 1 : Math.min(1, (now - start) / duration);
if (now - last > 50 || p === 1) {
last = now;
setGlyphs(
chars.map((c, i) =>
c === " " || p >= (locks[i] ?? 0) ? null : glyph(c, Math.random()),
),
);
}
if (p < 1) raf.current = requestAnimationFrame(step);
};
raf.current = requestAnimationFrame(step);
}, [text, duration, reduce]);
useEffect(() => {
if (seen) run();
return () => cancelAnimationFrame(raf.current);
}, [seen, run]);
const words = text.split(" ");
let offset = 0;
return (
<div className="w-full max-w-md">
<button
ref={ref}
type="button"
onClick={run}
aria-label={text}
className="block w-full cursor-pointer text-left text-balance text-[44px] font-semibold leading-[1.02] tracking-[-0.045em] text-[#1d1d1f] dark:text-[#f5f5f7] sm:text-[50px]"
>
<span aria-hidden="true">
{words.map((w, wi) => {
const start = offset;
offset += w.length + 1;
return (
<Fragment key={wi}>
<span className="inline-block whitespace-nowrap">
{[...w].map((ch, ci) => {
const g = glyphs[start + ci];
return (
<span key={ci} className="relative inline-block">
<span className="transition-opacity duration-300" style={{ opacity: g ? 0 : 1 }}>
{ch}
</span>
{g ? (
<span className="absolute inset-0 flex justify-center" style={{ color }}>
{g}
</span>
) : null}
</span>
);
})}
</span>
{wi < words.length - 1 ? " " : null}
</Fragment>
);
})}
</span>
</button>
<p className="mt-5 flex items-center gap-1.5 text-[13px] text-[#86868b]">
<RotateCcw strokeWidth={1.5} className="h-3.5 w-3.5" />
Touchez le titre pour le déchiffrer à nouveau
</p>
</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 de texte brouillé qui se déchiffre convient aux marques proches du code ou du secret : une société de cybersécurité, le portfolio d’un développeur, la page de lancement d’une API, un portefeuille crypto. Le titre se décode à son arrivée à l’écran et, comme chaque lettre garde sa place, rien ne bouge autour.
Les titres courts, de trois à six mots, rendent le mieux. Gardez une durée sous les deux secondes, sinon on attend au lieu de lire. Un toucher relance l’effet. Les lecteurs d’écran annoncent directement la vraie phrase, pas les caractères aléatoires.