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 →Preview
Source
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;
}
Dependencies
gsap
