UT Hero Section

The UT Hero section with sticky text and a parallax background

Installation

1

Install dependencies

npm install framer-motion tailwind-merge clsx
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
3

Copy the source code

components/ui/sticky-hero.tsx
'use client';
import React, { ReactNode, useRef, useState } from 'react'
import Image from 'next/image';

interface StickyHeroProps {
    heroText: ReactNode;
    srcHero: string;
    mediaType: string;
    srcBelow: string;
    children?: ReactNode;
}

export function StickyHero({
    heroText,
    srcHero,
    mediaType,
    srcBelow,
    children
}: StickyHeroProps) {
    const [isPlaying, setIsPlaying] = useState<boolean>(true);
    const videoRef = useRef<HTMLVideoElement | null>(null);

    const togglePlay = () => {
        if (mediaType === 'video' && videoRef.current) {
            if (isPlaying) {
                videoRef.current.pause();
            } else {
                videoRef.current.play();
            }
            setIsPlaying(!isPlaying);
        }
    };

    return (
        <div>
            <div className='absolute h-screen flex items-center justify-start'>
                {mediaType === 'video' ? (
                    <video
                        ref={videoRef}
                        className="h-full w-full object-cover"
                        autoPlay
                        muted
                        loop
                        playsInline
                        src={srcHero}
                    />
                ) : (
                    <Image
                        className="h-full w-full object-cover"
                        width={1920}
                        height={1080}
                        src={srcHero}
                        alt="Hero background"
                        priority
                    />
                )}

                {!isPlaying && (
                    <div className="absolute inset-0 bg-gradient-to-r from-black/40 to-transparent" />
                )}

                {mediaType === 'video' && (
                    <div className="absolute bottom-14 right-5 p-4 z-50">
                        <button
                            onClick={togglePlay}
                            className="rounded-full bg-white bg-opacity-40 p-3 hover:bg-opacity-60 transition-all cursor-pointer"
                            aria-label={isPlaying ? 'Pause' : 'Play'}
                        >
                            {isPlaying ? (
                                <svg className="w-8 h-8 text-black" viewBox="0 0 24 24" fill="currentColor">
                                    <path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z" />
                                </svg>
                            ) : (
                                <svg className="w-8 h-8 text-black" viewBox="0 0 24 24" fill="currentColor">
                                    <path d="M8 5v14l11-7z" />
                                </svg>
                            )}
                        </button>
                    </div>
                )}
            </div>
            <div className='relative h-[190vh]'>
                <div className="sticky z-50 top-[30%] w-full flex justify-center mb-[5vh]">
                    <h1
                        className='z-50 text-left sm:text-[5rem] text-[3rem] max-w-6xl w-full leading-[0.8] font-black text-white mx-[2%]'
                        style={{ textShadow: '1px 1px 1px rgba(51, 63, 72, 0.4)' }}
                    >
                        {heroText}
                    </h1>
                </div>
                <Image
                    className="z-[-2] fixed left-0 right-0 bottom-0 h-screen object-cover"
                    width={1920}
                    height={1080}
                    src={srcBelow}
                    alt="Hero background"
                    priority
                />
                <div className="absolute bottom-0 inset-x-0 h-screen bg-gradient-to-t from-[#fd7e14]/70 to-transparent" />
            </div>
            <div>
                {children}
            </div>
        </div>
    )
}

Props

PropTypeDescriptionDefault Value
heroTextReactNodeMain text for the heroundefined
srcHerostringSource path or link to the image or videoundefined
mediaTypestring"video" or "image" for the srcHero propundefined
srcBelowstringSource path or link to the image below the main image or videoundefined
childrenReactNodeComponents that will go under the heroText after its stickiness endsundefined

Inspired by UT Austin
Video Credits: UT Austin