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.

Version Markdown avec tout le code, pour les assistants IA

Plus dans Texte et défilement

Voir tout : 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.