Párrafo keynote
Un párrafo enorme que se enciende palabra a palabra con el scroll. Marca con *asteriscos* las palabras en color.
Ajustes
Código
import { Fragment, useRef } from "react";
import { motion, useScroll, useSpring, useTransform, type MotionValue } from "framer-motion";
interface KeynoteScrollTextProps {
/** Rodea con asteriscos las palabras que quieras destacar: *así*. */
text?: string;
accent?: string;
/** Opacidad (0-1) de las palabras que aún no se han leído. */
dim?: number;
/** true: caja con scroll propio. false: usa el scroll de la página. */
contained?: boolean;
}
function Word({
word,
progress,
range,
accent,
dim,
}: {
word: string;
progress: MotionValue<number>;
range: [number, number];
accent: string;
dim: number;
}) {
const opacity = useTransform(progress, range, [dim, 1]);
return (
<motion.span style={{ opacity, color: word.includes("*") ? accent : undefined }}>
{word.replace(/\*/g, "")}
</motion.span>
);
}
export default function KeynoteScrollText({
text = "Diseñamos cada detalle para que desaparezca. Lo que queda es la *sensación* de que todo *simplemente* *funciona.*",
accent = "#2997ff",
dim = 0.14,
contained = true,
}: KeynoteScrollTextProps) {
const scroller = useRef<HTMLDivElement>(null);
const target = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll(
contained
? { container: scroller, target, offset: ["start start", "end end"] }
: { target, offset: ["start start", "end end"] },
);
const progress = useSpring(scrollYProgress, { stiffness: 140, damping: 30, restDelta: 0.0005 });
const words = text.split(/\s+/).filter(Boolean);
const lead = 0.28;
const step = (0.94 + lead) / (words.length + 2);
const section = (
<div ref={target} className={contained ? "relative h-[320%]" : "relative h-[320vh]"}>
<div
className={
(contained ? "h-[31.25%]" : "h-screen") +
" sticky top-0 flex items-center px-[8%] [container-type:inline-size]"
}
>
<p className="text-[clamp(28px,10cqi,76px)] font-semibold leading-[1.08] tracking-[-0.035em]">
<span className="sr-only">{text.replace(/\*/g, "")}</span>
<span aria-hidden="true">
{words.map((word, i) => (
<Fragment key={word + "-" + i}>
<Word
word={word}
progress={progress}
range={[-lead + i * step, -lead + (i + 3) * step]}
accent={accent}
dim={dim}
/>{" "}
</Fragment>
))}
</span>
</p>
</div>
</div>
);
const theme = "bg-white text-[#1d1d1f] dark:bg-black dark:text-[#f5f5f7]";
if (!contained) return <div className={theme}>{section}</div>;
return (
<div
ref={scroller}
className={"relative h-[32rem] w-full overflow-y-auto rounded-3xl [scrollbar-width:none] " + theme}
>
{section}
</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
Ese texto que se ilumina con el scroll, como en las páginas de producto de Apple, funciona mejor con una sola frase con peso: el manifiesto de una agencia, el «por qué existimos» de una startup, el arranque de la página de quiénes somos. Las palabras pasan de tenues a sólidas al ritmo de quien lee.
Déjalo en una o dos frases; un párrafo entero se hace pesado de recorrer. Colorea solo dos o tres palabras. Los lectores de pantalla reciben el texto completo de golpe, y lo primero que conviene ajustar es la opacidad de lo que aún no se ha leído.