Nous concevons chaque détail pour qu’il disparaisse. Il reste la sensation que tout fonctionne simplement.

React + MotionTexte et défilement

Paragraphe keynote

Un très grand paragraphe s’allume mot à mot au défilement, comme une keynote. Entourez des mots d’*astérisques* pour les colorer.

Réglages

0.14

Code

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 = "Nous concevons chaque détail pour qu’il disparaisse. Il reste la *sensation* que tout *fonctionne* *simplement.*",
  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 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

Cet effet de texte qui s’éclaire au scroll, vu sur les pages produit d’Apple, marche le mieux avec une seule phrase forte : le manifeste d’une agence, le « pourquoi nous existons » d’une startup, la première ligne d’une page à propos. Les mots passent du pâle au plein au rythme du lecteur, et la phrase porte davantage.

Tenez-vous-en à une ou deux phrases ; un paragraphe entier devient pénible à faire défiler. Ne colorez que deux ou trois mots. Les lecteurs d’écran reçoivent tout le texte d’un coup, et le premier réglage à toucher est l’opacité des mots pas encore lus.