A pinned sentence whose key word changes as you scroll, with the space around it resizing smoothly.
Built for founders, agencies, studios, makers
Scroll inside
Customize
Usage
import { ScrollWordSwap } from "@/components/motionkit/scroll-word-swap";
// pins for one screen, then swaps a word every 60vh of scrolling
<ScrollWordSwap
before="Built for"
words={["founders", "agencies", "studios", "makers"]}
className="text-5xl font-semibold"
/>Motion
The exact curves this component runs, plotted with Motion's own maths.
Word swap
Scroll reaches the next word
Slot resize
Each swap
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/scroll-word-swap.tsx.
"use client";
import { useRef, useState, type RefObject } from "react";
import { AnimatePresence, motion, useMotionValueEvent, useReducedMotion, useScroll } from "motion/react";
type ScrollWordSwapProps = {
/** Text before the changing word. */
before: string;
/** The words that take turns, in scroll order. */
words: string[];
/** Text after the changing word. */
after?: string;
/** Colour of the changing word. */
color?: string;
/** Scroll distance per word, as a CSS length. */
step?: string;
/** Height of the pinned area, as a CSS length. Usually the screen. */
height?: string;
/** A scrolling element to follow instead of the page, e.g. a panel with overflow. */
container?: RefObject<HTMLElement | null>;
className?: string;
};
export function ScrollWordSwap({
before,
words,
after = "",
color = "#5FAEB4",
step = "60vh",
height = "100vh",
container,
className = "",
}: ScrollWordSwapProps) {
const ref = useRef<HTMLDivElement>(null);
const reduceMotion = useReducedMotion();
const [index, setIndex] = useState(0);
// 0 when the section's top reaches the top of the view, 1 when its end does
const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start start", "end end"] });
useMotionValueEvent(scrollYProgress, "change", (progress) => {
const next = Math.min(words.length - 1, Math.floor(progress * words.length));
if (next !== index) setIndex(next);
});
return (
// the section is tall so there is room to scroll; the sentence stays pinned while it does
<div ref={ref} className="relative" style={{ height: `calc(${height} + ${step} * ${Math.max(words.length - 1, 0)})` }}>
<div className={`sticky top-0 flex items-center justify-center ${className}`} style={{ height }}>
<p>
<span className="sr-only">{`${before} ${words.join(", ")} ${after}`}</span>
<span aria-hidden>
{before}{" "}
{/* the slot resizes to each word, so the words around it glide instead of jumping */}
<motion.span
layout={!reduceMotion}
transition={{ type: "spring", bounce: 0.15, duration: 0.5 }}
className="relative inline-flex overflow-hidden whitespace-nowrap pb-[0.1em] align-bottom"
>
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={words[index]}
style={{ color }}
initial={reduceMotion ? { opacity: 0 } : { y: "100%", opacity: 0, filter: "blur(6px)" }}
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
exit={reduceMotion ? { opacity: 0 } : { y: "-100%", opacity: 0, filter: "blur(6px)" }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
className="inline-block"
>
{words[index]}
</motion.span>
</AnimatePresence>
</motion.span>
{after && " "}
{after}
</span>
</p>
</div>
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| before | string | — | Text before the changing word. |
| words | string[] | — | Words that take turns, in scroll order. |
| after | string | "" | Text after the changing word. |
| color | string | "#5FAEB4" | Colour of the changing word. |
| step | string | "60vh" | Scroll distance per word. |
| height | string | "100vh" | Height of the pinned area. |
| container | RefObject<HTMLElement> | — | A scrolling element to follow instead of the page. |
Small details, big impact.
Kinetic Text
Headlines that rise letter by letter, slide up line by line, blur into focus or light up as you scroll.
Think beyond ordinary.
Gradient Text
Headline text with a slowly flowing, seamlessly looping gradient.
Text Scramble
Text that decodes itself character by character, terminal style.