Command Palette

Search for a command to run...

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 !