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 →Preview
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.
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.
Source
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>
);
}
"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>
);
}
/* ── 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;
}
Dependencies
gsap