HeroesIntermediateMay 11, 2026

Hero Home

Full-viewport video hero with inset rounded background on desktop, large fluid display typography stacked line-by-line, and a glassmorphic CTA card anchored bottom-right. Inspired by Wolverine Worldwide's homepage.

View Full Demo →

2025 Annual Report
latest news
2025 Annual Report
demo.jsx
import HeroHome from "./index.jsx";
import { heroHome } from "../demo-data.js";

export default function HeroHomeDemo() {
  return <HeroHome {...heroHome} />;
}
index.jsx
"use client";

import { useEffect, useRef } from "react";
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
import styles from "./styles.module.css";

gsap.registerPlugin(SplitText);

export default function HeroHome({
  videoSrc,
  posterSrc,
  headline = ["Make.", "Every Day.", "Better."],
  card = {},
}) {
  const sectionRef = useRef(null);

  useEffect(() => {
    const el = sectionRef.current;
    if (!el) return;

    const ctx = gsap.context(() => {
      const lines = el.querySelectorAll("[data-hero-home-reveal]");

      gsap.set(lines, { visibility: "visible" });

      lines.forEach((line) => {
        SplitText.create(line, {
          type: "lines",
          mask: "lines",
          autoSplit: true,
          linesClass: "line",
          onSplit(instance) {
            gsap.from(instance.lines, {
              yPercent: 110,
              duration: 1,
              stagger: 0.06,
              delay: 0.3 + Array.from(lines).indexOf(line) * 0.12,
              ease: "expo.out",
            });
          },
        });
      });

      const cardEl = el.querySelector("[data-hero-home-card]");
      if (cardEl) {
        gsap.from(cardEl, {
          y: 40,
          opacity: 0,
          duration: 1,
          delay: 0.8,
          ease: "expo.out",
        });
      }
    }, el);

    return () => ctx.revert();
  }, []);

  const {
    image: cardImage,
    label: cardLabel = "latest news",
    title: cardTitle = "2025 Annual Report",
    href: cardHref = "#",
  } = card;

  return (
    <div className={styles.wrapper}>
      <section ref={sectionRef} className={styles.hero}>
        <h1 className={styles.title} aria-label={headline.join(" ")}>
          {headline.map((line, i) => (
            <div key={i} className={styles.titleLine} aria-hidden="true">
              <span className={styles.titleText} data-hero-home-reveal>
                {line}
              </span>
            </div>
          ))}
        </h1>

        <a
          className={styles.card}
          href={cardHref}
          target="_blank"
          rel="noopener noreferrer"
          data-hero-home-card
        >
          {cardImage && (
            <div className={styles.cardImageWrap}>
              <img
                className={styles.cardImage}
                src={cardImage}
                alt={cardTitle}
                loading="lazy"
              />
            </div>
          )}
          <div className={styles.cardBody}>
            <span className={styles.cardLabel}>{cardLabel}</span>
            <div className={styles.cardBottom}>
              <span className={styles.cardTitle}>{cardTitle}</span>
              <span className={styles.cardIcon}>
                <svg
                  width="16"
                  height="16"
                  viewBox="0 0 16 16"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M4.5 11.5L11.5 4.5M11.5 4.5H5.5M11.5 4.5V10.5"
                    stroke="currentColor"
                    strokeWidth="1.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
              </span>
            </div>
          </div>
        </a>

        <div className={styles.background}>
          {videoSrc ? (
            <video
              className={styles.backgroundVideo}
              poster={posterSrc}
              autoPlay
              loop
              muted
              playsInline
              disablePictureInPicture
              preload="none"
              tabIndex={-1}
            >
              <source src={videoSrc} type="video/mp4" />
            </video>
          ) : posterSrc ? (
            <img
              className={styles.backgroundPoster}
              src={posterSrc}
              alt=""
              loading="eager"
            />
          ) : null}
          <div className={styles.gradient} />
        </div>
      </section>
    </div>
  );
}
styles.module.css
/* ── Outer wrapper — inset on desktop ── */
.wrapper {
  display: block;
  height: 100%;
  width: 100%;
}

@media (min-width: 768px) {
  .wrapper {
    padding-left: 8px;
    padding-right: 8px;
    padding-top: 8px;
  }
}

/* ── Hero section ── */
.hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100svh;
  width: 100%;
  overflow: hidden;
  padding: 0 12px 16px;
  padding-top: 40svh;
  color: #ffffff;
}

@media (min-width: 768px) {
  .hero {
    flex-direction: row;
    align-items: flex-end;
    justify-content: space-between;
    padding: 0 40px 48px;
  }
}

/* ── Title ── */
.title {
  display: flex;
  flex-direction: column;
  line-height: 0.92;
  letter-spacing: -0.02em;
  font-weight: 500;
}

.titleLine {
  position: relative;
  display: block;
  text-align: start;
}

.titleText {
  font-size: clamp(2.75rem, 0.9239rem + 9.1304vw, 8rem);
}

@media (min-width: 768px) {
  .titleText {
    font-size: clamp(6.25rem, 4.7717rem + 7.3913vw, 10.5rem);
  }
}

/* ── Card CTA ── */
.card {
  display: flex;
  flex-direction: row;
  width: 100%;
  gap: 4px;
  border-radius: 12px;
  background: rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  padding: 4px;
  text-decoration: none;
  color: #ffffff;
  margin-top: 16px;
}

@media (min-width: 640px) {
  .card {
    width: 280px;
    flex-direction: column;
    gap: 4px;
    margin-top: 0;
    flex-shrink: 0;
  }
}

/* ── Card image ── */
.cardImageWrap {
  width: 40%;
  aspect-ratio: 1.6;
  overflow: hidden;
  border-radius: 8px;
  flex-shrink: 0;
}

@media (min-width: 640px) {
  .cardImageWrap {
    width: 100%;
  }
}

.cardImage {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
}

.card:hover .cardImage {
  transform: scale(1.05);
}

/* ── Card body ── */
.cardBody {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 4px 0;
  flex: 1;
  min-width: 0;
}

@media (min-width: 640px) {
  .cardBody {
    gap: 40px;
    padding: 8px;
  }
}

.cardLabel {
  font-size: clamp(0.625rem, 0.6033rem + 0.1087vw, 0.6875rem);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.9;
}

.cardBottom {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
}

.cardTitle {
  font-size: clamp(0.875rem, 0.7663rem + 0.5435vw, 1.1875rem);
  font-weight: 700;
  max-width: 70%;
  line-height: 1.2;
}

@media (min-width: 640px) {
  .cardTitle {
    font-size: clamp(1.5rem, 1.5rem, 1.5rem);
  }
}

.cardIcon {
  padding-bottom: 4px;
  transition: transform 0.6s cubic-bezier(0.19, 1, 0.22, 1);
  flex-shrink: 0;
}

.card:hover .cardIcon {
  transform: scale(1.25);
}

/* ── Background layer ── */
.background {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}

@media (min-width: 768px) {
  .background {
    border-radius: 20px;
  }
}

.backgroundVideo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.backgroundPoster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── Gradient overlay on background ── */
.gradient {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(
      180deg,
      rgba(0, 0, 0, 0.2) 0%,
      rgba(0, 0, 0, 0) 22%
    ),
    linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%);
}

/* ── Text reveal (SplitText) ── */
:global([data-hero-home-reveal]) {
  visibility: hidden;
}

:global([data-hero-home-reveal]) > * {
  margin-bottom: -0.1em;
}

:global(.line-mask) > *,
:global(.word-mask) > *,
:global(.char-mask) > * {
  padding-bottom: 0.1em;
  will-change: transform;
}
  • gsap

May 11, 2026

HEROES

Hero About

Scroll-driven parallax hero where a full-bleed video shrinks and zooms out as text transitions from white-on-video to black-on-white. Three-column layout with staggered divider and description reveals in the final 30% of scroll. Separate stacked mobile layout with scroll-triggered entrances.

May 4, 2026

HEROES

Hero Brand

A full-viewport brand hero section with large statement headline, live clock, and CTA bar. Supports three color variants (default yellow, dark, sage green) and optional background image.

Apr 27, 2026

HEROES

Hero 01

Full-viewport hero with a GSAP clip-path reveal on the background image, overlay fade-in, and SplitText line-by-line text entrance. The media layer expands from a narrow vertical slit to full screen while the image scales down then back up.