Wir gestalten jedes Detail so, dass es verschwindet. Was bleibt, ist das Gefühl, dass alles einfach funktioniert.

React + MotionText & Scroll

Keynote-Absatz beim Scrollen

Ein riesiger Absatz leuchtet beim Scrollen Wort für Wort auf, wie auf einer Keynote-Folie. Mit *Sternchen* färbst du Wörter ein.

Einstellungen

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 = "Wir gestalten jedes Detail so, dass es verschwindet. Was bleibt, ist das *Gefühl*, dass alles *einfach* *funktioniert.*",
  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 für KI

Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.

Mehr aus Text & Scroll

Wofür du ihn nutzen kannst

Diesen Text-Highlight-Effekt beim Scrollen kennt man von Apples Produktseiten, und er wirkt am besten bei einer einzigen starken Aussage: das Manifest auf der Landingpage einer Agentur, das „Warum es uns gibt“ eines Startups, der erste Satz der Über-uns-Seite. Die Wörter werden im Tempo der Lesenden von blass zu kräftig.

Bleib bei ein oder zwei Sätzen, ein ganzer Absatz wird zum Scroll-Marathon. Färbe nur zwei oder drei Wörter mit Sternchen ein. Screenreader bekommen den ganzen Text auf einmal. Als Erstes lohnt es sich, die Deckkraft der ungelesenen Wörter anzupassen.