Scroll Velocity Hero
A hero section with scroll velocity-driven text skew effects, bold typography, with theme toggle.
Installation
Install required dependencies
npm i motion clsx tailwind-merge
Copy the following source code
"use client";
import Link from 'next/link';
import { useEffect, useRef, useState } from 'react';
import { useTheme } from 'next-themes';
import { Poppins } from 'next/font/google';
import { motion, useScroll, useTransform, useSpring, useVelocity } from 'motion/react';
import { Moon, Sun, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
const poppins = Poppins({
weight: ['300', '400', '500', '700', '900'],
subsets: ['latin'],
display: 'swap',
});
const ScrollVelocityHero = () => {
const { theme, setTheme } = useTheme();
const containerRef = useRef(null);
const textRef = useRef<HTMLParagraphElement>(null);
const [mounted, setMounted] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const [maxTranslateX, setMaxTranslateX] = useState(-2700);
const [containerHeight, setContainerHeight] = useState(0);
const { scrollYProgress } = useScroll({
container: containerRef,
offset: ["start start", "end start"]
});
const smoothProgress = useSpring(scrollYProgress, {
stiffness: 200,
damping: 40,
restDelta: 0.0001
});
const scrollVelocity = useVelocity(smoothProgress);
const paragraphX = useTransform(smoothProgress, [0, 0.75], [0, maxTranslateX]);
const skewpara = useTransform(
scrollVelocity,
[-0.7, 0.7],
[35, -35],
{ clamp: false }
);
useEffect(() => {
setMounted(true);
const timer = setTimeout(() => setIsInitialized(true), 150);
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (!isInitialized || !textRef.current) return;
const updateDimensions = () => {
const textWidth = textRef.current?.scrollWidth || 0;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const newMaxTranslateX = -(textWidth - viewportWidth);
const newContainerHeight = (Math.abs(newMaxTranslateX) / 0.8 / viewportHeight) * 100;
setMaxTranslateX(newMaxTranslateX);
setContainerHeight(newContainerHeight);
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
return () => window.removeEventListener('resize', updateDimensions);
}, [isInitialized]);
if (!mounted) return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-br from-indigo-900 via-purple-900 to-slate-900">
<Sparkles className="w-12 h-12 text-sky-400 animate-pulse" />
</div>
);
return (
<main className={`${poppins.className} antialiased`}>
<section
ref={containerRef}
style={{ height: `${containerHeight}vh` }}
className={`
bg-gradient-to-br from-slate-100 via-sky-50 to-purple-100 // Light theme gradient
text-indigo-900
dark:from-indigo-950 dark:via-slate-900 dark:to-purple-950 // Dark theme gradient
dark:text-sky-300
transition-colors duration-500 ease-in-out
`}
>
<div className="sticky top-0 flex h-screen flex-col justify-between overflow-hidden">
<div className="relative z-10 flex w-full items-center justify-between p-6 md:p-8">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="h-10 w-10 cursor-pointer rounded-full !bg-transparent text-indigo-700 shadow-none
hover:!bg-sky-100/50 focus-visible:!ring-2 focus-visible:!ring-sky-500 focus-visible:!ring-offset-0
dark:text-sky-400 dark:hover:!bg-indigo-800/50 dark:focus-visible:!ring-sky-400"
>
{theme === "light" ? (
<Moon className="h-5 w-5 transition-all" />
) : (
<Sun className="h-5 w-5 transition-all" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
<nav className="flex gap-4 md:gap-6 text-sm font-medium text-indigo-800 dark:text-sky-200">
<Link href="#" className="transition-colors hover:text-purple-600 dark:hover:text-purple-400 group">
Discover <span className="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-purple-500 dark:bg-purple-400"></span>
</Link>
<Link href="#" className="transition-colors hover:text-purple-600 dark:hover:text-purple-400 group">
Showcase <span className="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-purple-500 dark:bg-purple-400"></span>
</Link>
<Link href="#" className="transition-colors hover:text-purple-600 dark:hover:text-purple-400 group">
Labs <span className="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-purple-500 dark:bg-purple-400"></span>
</Link>
</nav>
</div>
<div className="flex flex-col items-center justify-center text-center px-4 my-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2, ease: "easeOut" }}
className="relative z-1"
>
<h1 className="text-3xl font-bold text-slate-700
sm:text-4xl md:text-5xl
lg:text-6xl xl:text-7xl 2xl:text-8xl
dark:text-sky-100
leading-snug sm:leading-tight">
Journey Beyond. <br />
Explore the Unseen. <br />
<span className="inline-block font-black text-transparent bg-clip-text
bg-gradient-to-r from-purple-600 via-pink-500 to-orange-400
dark:from-sky-400 dark:via-pink-400 dark:to-red-400
py-2 -skew-x-[15deg]
">
HORIZONS.
</span>
</h1>
</motion.div>
</div>
{isInitialized && (
<motion.p
ref={textRef}
className="origin-bottom-left whitespace-nowrap text-6xl font-black uppercase
leading-[0.8] text-indigo-400/70
md:text-8xl lg:text-9xl md:leading-[0.8]
dark:text-sky-500/60
select-none
"
style={{
x: paragraphX,
skewX: skewpara,
}}
>
DESIGN IS THE SILENT AMBASSADOR OF YOUR BRAND. EXPLORE NEW REALMS.
</motion.p>
)}
<div className="absolute left-6 top-1/2 hidden -translate-y-1/2 text-xs text-indigo-700 lg:flex flex-col items-center space-y-1 dark:text-sky-400">
<span style={{ writingMode: 'vertical-lr' }} className="opacity-75 tracking-wider">SCROLL</span>
<motion.svg
animate={{ y: [0, 5, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5 opacity-75"
>
<line x1="12" y1="5" x2="12" y2="19" />
<polyline points="19 12 12 19 5 12" />
</motion.svg>
</div>
<div className="absolute right-6 top-1/2 hidden -translate-y-1/2 text-xs text-indigo-700 lg:flex flex-col items-center space-y-1 dark:text-sky-400">
<span style={{ writingMode: 'vertical-lr' }} className="opacity-75 tracking-wider">DISCOVER</span>
<motion.svg
animate={{ y: [0, 5, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5 opacity-75"
>
<line x1="12" y1="5" x2="12" y2="19" />
<polyline points="19 12 12 19 5 12" />
</motion.svg>
</div>
</div>
</section>
<section className="h-screen bg-gradient-to-br from-purple-950 via-slate-900 to-indigo-950 flex items-center justify-center">
<h2 className="text-5xl text-sky-300 font-bold">Next Section</h2>
</section>
</main>
);
};
export default ScrollVelocityHero;
Now paste it into your project and thats it !