HeroesAdvancedMay 11, 2026

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.

View Full Demo →

Make. Every Day. Better.

We make great things. Thoughtful, trend-right, versatile, innovative products and experiences that improve our consumers' lives.

Every day matters. Make amazing every day. Progress every day. Grow every day. For a better tomorrow.

Our consumers are moved to live healthier and more productive lives. Our brands help them to do just that. And we are inspired by them to get better every day ourselves, and make those around us better.

Make.

We make great things. Thoughtful, trend-right, versatile, innovative products and experiences that improve our consumers' lives.

Every Day.

Every day matters. Make amazing every day. Progress every day. Grow every day. For a better tomorrow.

Better.

Our consumers are moved to live healthier and more productive lives. Our brands help them to do just that. And we are inspired by them to get better every day ourselves, and make those around us better.

demo.jsx
import HeroAbout from "./index.jsx";
import { heroAbout } from "../demo-data.js";

export default function HeroAboutDemo() {
  return (
    <div>
      <HeroAbout {...heroAbout} />
      {/* Spacer so you can scroll past the hero */}
      <div style={{ height: "100vh" }} />
    </div>
  );
}
index.jsx
"use client";

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

gsap.registerPlugin(ScrollTrigger);

export default function HeroAbout({
  videoSrc,
  posterSrc,
  columns = [
    {
      heading: "Make.",
      description:
        "We make great things. Thoughtful, trend-right, versatile, innovative products and experiences that improve our consumers' lives.",
    },
    {
      heading: "Every Day.",
      description:
        "Every day matters. Make amazing every day. Progress every day. Grow every day. For a better tomorrow.",
    },
    {
      heading: "Better.",
      description:
        "Our consumers are moved to live healthier and more productive lives. Our brands help them to do just that. And we are inspired by them to get better every day ourselves, and make those around us better.",
    },
  ],
}) {
  const sectionRef = useRef(null);
  const scrollRef = useRef(null);

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

    const mm = gsap.matchMedia();

    // Desktop: drive --progress with ScrollTrigger
    mm.add("(min-width: 768px)", () => {
      ScrollTrigger.create({
        trigger: scrollEl,
        start: "top top",
        end: "bottom bottom",
        scrub: true,
        onUpdate(self) {
          scrollEl.style.setProperty("--progress", self.progress);
        },
      });
    });

    // Mobile: animate each block on scroll-in
    mm.add("(max-width: 767px)", () => {
      const blocks = el.querySelectorAll("[data-mobile-block]");

      blocks.forEach((block, i) => {
        const divider = block.querySelector("[data-mobile-divider]");
        const heading = block.querySelector("[data-mobile-heading]");
        const desc = block.querySelector("[data-mobile-desc]");

        const tl = gsap.timeline({
          scrollTrigger: {
            trigger: block,
            start: "top 85%",
            once: true,
          },
        });

        if (divider) {
          tl.to(divider, {
            scaleY: 1,
            duration: 0.8,
            ease: "power3.out",
          }, 0);
        }

        if (heading) {
          tl.from(heading, {
            yPercent: 100,
            opacity: 0,
            duration: 0.8,
            ease: "expo.out",
          }, 0.1);
        }

        if (desc) {
          tl.to(desc, {
            opacity: 1,
            y: 0,
            duration: 0.8,
            ease: "expo.out",
          }, 0.3);
        }
      });
    });

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

  const fullHeadline = columns.map((c) => c.heading).join(" ");

  return (
    <section ref={sectionRef} className={styles.section}>
      <h1 className={styles.srOnly}>{fullHeadline}</h1>

      <div className={styles.inset}>
        <div ref={scrollRef} className={styles.scrollContainer}>
          {/* Sticky panel with columns */}
          <div className={styles.stickyPanel}>
            <div className={styles.columns}>
              {columns.map((col, i) => (
                <div key={i} className={styles.column}>
                  <span className={styles.divider} />
                  <span className={styles.heading} aria-hidden="true">
                    {col.heading}
                  </span>
                  <div className={styles.description}>
                    <p>{col.description}</p>
                  </div>
                </div>
              ))}
            </div>
          </div>

          {/* Video background */}
          <div className={styles.background}>
            <div className={styles.videoWrap}>
              {videoSrc ? (
                <video
                  className={styles.video}
                  poster={posterSrc}
                  autoPlay
                  loop
                  muted
                  playsInline
                  disablePictureInPicture
                  preload="none"
                  tabIndex={-1}
                >
                  <source src={videoSrc} type="video/mp4" />
                </video>
              ) : posterSrc ? (
                <img
                  className={styles.poster}
                  src={posterSrc}
                  alt=""
                  loading="eager"
                />
              ) : null}
            </div>
            <div className={styles.gradient} />
          </div>
        </div>

        {/* Mobile stacked columns (below video) */}
        <div className={styles.mobileColumns}>
          {columns.map((col, i) => (
            <div key={i} className={styles.mobileBlock} data-mobile-block>
              <span
                className={styles.mobileDivider}
                data-mobile-divider
              />
              <span className={styles.mobileHeading} data-mobile-heading>
                {col.heading}
              </span>
              <div
                className={styles.mobileDescription}
                data-mobile-desc
                style={{ transform: "translateY(1rem)" }}
              >
                <p>{col.description}</p>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
styles.module.css
/* ── Root ── */
.section {
  position: relative;
}

/* ── Inset wrapper (desktop) ── */
.inset {
  /* No padding on mobile */
}

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

/* ── Scroll container — tall on desktop for scroll room ── */
.scrollContainer {
  position: relative;
}

@media (min-width: 768px) {
  .scrollContainer {
    height: 250svh;
  }
}

/* ── Sticky content panel ── */
.stickyPanel {
  display: flex;
  height: 100svh;
  width: 100%;
  align-items: center;
  padding-left: 12px;
  padding-right: 12px;
  color: #ffffff;
}

@media (min-width: 768px) {
  .stickyPanel {
    position: sticky;
    top: 0;
    padding-left: 40px;
    padding-right: 40px;
    /* Color transitions from white → black based on --progress */
    color: hsl(0 0% calc((1 - var(--progress, 0)) * 100%));
  }
}

/* ── Columns grid ── */
.columns {
  display: flex;
  width: 100%;
  justify-content: space-between;
}

@media (min-width: 768px) {
  .columns {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
    transform: translateY(calc((1 - var(--progress, 0)) * 50%));
  }
}

/* ── Single column ── */
.column {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(5.625rem, 4.3207rem + 6.5217vw, 9.375rem);
}

/* ── Vertical divider ── */
.divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 1px;
  background-color: #cccccc;
  transform-origin: top;
  display: none;
}

@media (min-width: 768px) {
  .divider {
    display: block;
    transform: scaleY(clamp(0, calc((var(--progress, 0) - 0.7) * 5), 1));
  }
}

/* ── Heading text (desktop overlay) ── */
.heading {
  font-size: clamp(1.5rem, 0.7826rem + 3.587vw, 3.5625rem);
  font-weight: 500;
  line-height: 1.1;
  white-space: nowrap;
}

@media (min-width: 768px) {
  .heading {
    padding-left: 12px;
  }
}

/* ── Description text (desktop only, fades in late) ── */
.description {
  display: none;
}

@media (min-width: 768px) {
  .description {
    display: block;
    padding-left: 12px;
    font-size: clamp(1.5rem, 1.5rem, 1.5rem);
    font-weight: 700;
    line-height: 1.35;
    color: #757575;
    opacity: clamp(0, calc((var(--progress, 0) - 0.7) * 5), 1);
    transform: translateY(
      calc((1 - clamp(0, calc((var(--progress, 0) - 0.7) * 5), 1)) * 1rem)
    );
  }
}

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

@media (min-width: 768px) {
  .background {
    border-radius: 20px;
    transform: scale(calc(1 - var(--progress, 0) * 0.4));
  }
}

.videoWrap {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}

@media (min-width: 768px) {
  .videoWrap {
    transform: scale(calc(1 + var(--progress, 0) * 0.4));
  }
}

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

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

.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%);
}

/* ── Mobile stacked section (below video) ── */
.mobileColumns {
  display: flex;
  flex-direction: column;
  gap: clamp(5.625rem, 4.3207rem + 6.5217vw, 9.375rem);
  margin-top: clamp(5.625rem, 4.3207rem + 6.5217vw, 9.375rem);
  padding-left: 12px;
  padding-right: 12px;
}

@media (min-width: 768px) {
  .mobileColumns {
    display: none;
  }
}

.mobileBlock {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(5.625rem, 4.3207rem + 6.5217vw, 9.375rem);
}

.mobileDivider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 1px;
  background-color: #cccccc;
  transform-origin: top;
  transform: scaleY(0);
}

.mobileHeading {
  padding-left: 12px;
  font-size: clamp(1.5rem, 0.7826rem + 3.587vw, 3.5625rem);
  font-weight: 500;
  line-height: 1.1;
  white-space: nowrap;
}

.mobileDescription {
  padding-left: 12px;
  font-weight: 700;
  color: #757575;
  line-height: 1.35;
  opacity: 0;
}

/* ── Screen-reader only h1 ── */
.srOnly {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
  • gsap

May 11, 2026

HEROES

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.

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.