Scroll Text Highlight
Original · freeA statement whose words light up one by one as you scroll through the section.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/scroll-text-highlight.json"use client";
import { useRef, type ReactNode } from "react";
import {
motion,
useScroll,
useSpring,
useTransform,
type MotionValue,
} from "motion/react";
type WordProps = {
children: ReactNode;
progress: MotionValue<number>;
start: number;
end: number;
};
function Word({ children, progress, start, end }: WordProps) {
const opacity = useTransform(progress, [start, end], [1, 0]);
const revealed = useTransform(progress, [start, end], [0, 1]);
const blur = useTransform(revealed, [0, 1], [6, 0]);
const filter = useTransform(blur, (b: number) => `blur(${b}px)`);
const y = useTransform(revealed, [0, 1], [4, 0]);
return (
<span className="relative mr-[0.28em] inline-block leading-[1.18]">
{/* muted resting layer */}
<motion.span
aria-hidden
style={{ opacity }}
className="text-slate-300 dark:text-slate-700"
>
{children}
</motion.span>
{/* highlighted layer that lights up on scroll */}
<motion.span
aria-hidden
style={{ opacity: revealed, filter, y }}
className="absolute left-0 top-0 bg-gradient-to-br from-indigo-500 via-violet-500 to-fuchsia-500 bg-clip-text text-transparent dark:from-indigo-300 dark:via-violet-300 dark:to-fuchsia-300"
>
{children}
</motion.span>
</span>
);
}
export default function ScrollTextHighlight() {
const targetRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: targetRef,
offset: ["start start", "end end"],
});
const smooth = useSpring(scrollYProgress, {
stiffness: 90,
damping: 26,
mass: 0.4,
});
const barScaleX = useSpring(scrollYProgress, {
stiffness: 120,
damping: 30,
mass: 0.3,
});
const statement =
"We design and build websites that turn quiet traffic into paying customers — fast to load, easy to find, and impossible to ignore.";
const words = statement.split(" ");
// Reserve the last slice of scroll so the sentence finishes fully lit
// before the section unpins.
const revealWindow = 0.82;
const perWord = revealWindow / words.length;
const percent = useTransform(smooth, (v: number) =>
Math.round(Math.min(1, v / revealWindow) * 100)
);
return (
<section className="relative w-full bg-white dark:bg-slate-950">
<style>{`
@keyframes szh-hint {
0%, 100% { transform: translateY(0); opacity: 0.55; }
50% { transform: translateY(6px); opacity: 1; }
}
@keyframes szh-orb {
0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
50% { transform: translate3d(2%, -3%, 0) scale(1.08); }
}
.szh-hint { animation: szh-hint 1.8s ease-in-out infinite; }
.szh-orb-a { animation: szh-orb 14s ease-in-out infinite; }
.szh-orb-b { animation: szh-orb 18s ease-in-out infinite reverse; }
@media (prefers-reduced-motion: reduce) {
.szh-hint, .szh-orb-a, .szh-orb-b { animation: none !important; }
}
`}</style>
{/* tall scroll track — provides room for the word-by-word reveal */}
<div ref={targetRef} className="relative h-[320vh]">
{/* pinned stage */}
<div className="sticky top-0 flex h-screen items-center overflow-hidden">
{/* ambient background */}
<div aria-hidden className="pointer-events-none absolute inset-0">
<div className="szh-orb-a absolute -left-24 top-1/4 h-72 w-72 rounded-full bg-indigo-300/40 blur-3xl dark:bg-indigo-600/20" />
<div className="szh-orb-b absolute -right-16 bottom-1/4 h-80 w-80 rounded-full bg-fuchsia-300/40 blur-3xl dark:bg-fuchsia-600/20" />
<div
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
style={{
backgroundImage:
"radial-gradient(circle at 1px 1px, rgb(148 163 184 / 0.25) 1px, transparent 0)",
backgroundSize: "28px 28px",
}}
/>
</div>
<div className="relative mx-auto w-full max-w-6xl px-6 py-16 sm:px-10 lg:px-16">
{/* eyebrow + live counter */}
<div className="mb-10 flex flex-wrap items-center justify-between gap-4">
<div className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-4 py-1.5 text-xs font-medium uppercase tracking-[0.2em] text-slate-500 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-400">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
Our Promise
</div>
<div className="inline-flex items-baseline gap-1 font-mono text-sm text-slate-400 dark:text-slate-500">
<motion.span className="tabular-nums text-slate-700 dark:text-slate-200">
{percent}
</motion.span>
<span aria-hidden>%</span>
<span className="sr-only">of statement revealed</span>
</div>
</div>
{/* the statement that lights up word-by-word */}
<h2 className="max-w-5xl text-balance text-3xl font-semibold tracking-tight sm:text-4xl md:text-5xl lg:text-6xl">
<span className="sr-only">{statement}</span>
<span aria-hidden className="flex flex-wrap">
{words.map((word, i) => {
const start = i * perWord;
const end = start + perWord * 2.4;
return (
<Word
key={`${word}-${i}`}
progress={smooth}
start={start}
end={Math.min(end, 1)}
>
{word}
</Word>
);
})}
</span>
</h2>
{/* progress rail */}
<div className="mt-14 flex items-center gap-4">
<div className="h-[3px] flex-1 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<motion.div
aria-hidden
style={{ scaleX: barScaleX }}
className="h-full origin-left rounded-full bg-gradient-to-r from-indigo-500 via-violet-500 to-fuchsia-500"
/>
</div>
<span className="szh-hint hidden shrink-0 items-center gap-2 text-sm font-medium text-slate-400 sm:inline-flex dark:text-slate-500">
Keep scrolling
<svg
aria-hidden
viewBox="0 0 24 24"
fill="none"
className="h-4 w-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 5v14" />
<path d="m19 12-7 7-7-7" />
</svg>
</span>
</div>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Scroll Progress Reveal
OriginalA section that reveals its content in sequence as you scroll, with a sticky scroll-progress bar showing how far through it you are.

Parallax Layers
OriginalA layered depth scene where background, midground and foreground drift at different speeds, driven by scroll position.

Pinned Scroll Steps
OriginalA pinned, sticky panel whose visual and copy change step by step as you scroll past it, a classic scrollytelling section.

Horizontal Scroll Gallery
OriginalVertical scrolling pans a horizontal rail of cards sideways while the section is pinned, then releases.

Scroll Reveal Timeline
OriginalA vertical timeline whose connecting line fills and whose milestones fade in as they enter the viewport.

Count-up Stats Band
OriginalA metrics band whose numbers count up from zero the moment it scrolls into view.

Clip-path Image Reveal
OriginalPanels that unmask with a clip-path wipe as they enter the viewport, revealing the media beneath.

Stacking Cards
OriginalA deck of cards that pin and scale as you scroll, each settling behind the next.

Scroll Zoom Hero
OriginalA hero whose backdrop scales and brightens while the headline parallaxes upward as you scroll.

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

