AnimationsIntermediateApril 9, 2026
Horizontal Scroll
Full-viewport horizontal scroll section driven by GSAP ScrollTrigger. Panels are pinned and translated on the x-axis as you scroll vertically. Stacks vertically on mobile.
View Full Demo →Preview

Editorial

Campaign

Portrait

Studio

Archive
Source
demo.jsx
import HorizontalScroll from './index.jsx';
import styles from './demo.module.css';
const SLIDES = [
{ src: '/demo-assets/models/model0.png', alt: '', title: 'Editorial' },
{ src: '/demo-assets/models/model3.png', alt: '', title: 'Campaign' },
{ src: '/demo-assets/models/model6.png', alt: '', title: 'Portrait' },
{ src: '/demo-assets/models/model1.png', alt: '', title: 'Studio' },
{ src: '/demo-assets/models/model5.png', alt: '', title: 'Archive' },
];
export default function HorizontalScrollDemo() {
return (
<div className={styles.demo}>
<section className={styles.intro}>
<p className={styles.label}>Horizontal Scroll</p>
<h1 className={styles.heading}>Scroll down<br />to move through</h1>
</section>
<HorizontalScroll slides={SLIDES} />
<section className={styles.outro}>
<p className={styles.outroText}>End of demo</p>
</section>
</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';
export default function HorizontalScroll({ slides = [] }) {
const wrapRef = useRef(null);
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
const wrap = wrapRef.current;
if (!wrap) return;
const mm = gsap.matchMedia();
mm.add('(min-width: 768px)', () => {
const panels = gsap.utils.toArray('[data-panel]', wrap);
if (panels.length < 2) return;
const tween = gsap.to(panels, {
x: () => -(wrap.scrollWidth - window.innerWidth),
ease: 'none',
scrollTrigger: {
trigger: wrap,
start: 'top top',
end: () => '+=' + (wrap.scrollWidth - window.innerWidth),
scrub: true,
pin: true,
invalidateOnRefresh: true,
},
});
return () => {
tween.scrollTrigger?.kill();
tween.kill();
};
});
return () => mm.revert();
}, []);
return (
<section className={styles.wrap} ref={wrapRef}>
{slides.map((slide, i) => (
<div key={i} data-panel className={styles.panel}>
<div className={styles.panelInner}>
<div className={styles.card}>
<div className={styles.cardBg}>
<img
src={slide.src}
alt={slide.alt}
className={styles.cardBgImg}
/>
</div>
<div className={styles.cardInner}>
<h2 className={styles.cardTitle}>{slide.title}</h2>
</div>
</div>
</div>
</div>
))}
</section>
);
}
demo.module.css
.demo {
background: var(--color-bg);
}
.intro {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
padding: var(--space-16);
}
.label {
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-tertiary);
margin-bottom: var(--space-6);
}
.heading {
font-size: clamp(2.5rem, 6vw, 5rem);
font-weight: var(--weight-bold);
letter-spacing: -0.03em;
line-height: var(--leading-tight);
color: var(--color-text);
}
.outro {
display: flex;
align-items: center;
justify-content: center;
min-height: 50vh;
}
.outroText {
font-size: var(--text-sm);
color: var(--color-text-tertiary);
letter-spacing: 0.05em;
}
styles.module.css
/* Local tokens — component is self-contained */
.wrap {
--color-white: #ffffff;
--space-l: 1.25rem;
--space-5xl: 3rem;
--font-h1: clamp(3rem, 7vw, 6rem);
--font-h2: clamp(2rem, 5vw, 3.5rem);
display: flex;
flex-flow: row;
min-height: 100dvh;
overflow: hidden;
}
.panel {
flex: none;
width: 100%;
}
.panelInner {
width: 100%;
height: 100%;
padding: var(--space-l);
}
.card {
border-radius: var(--space-l);
display: flex;
flex-flow: column;
justify-content: flex-end;
align-items: flex-start;
width: 100%;
height: 100%;
padding: var(--space-5xl);
position: relative;
overflow: hidden;
}
.cardBg {
position: absolute;
inset: 0;
z-index: 0;
}
.cardBgImg {
width: 100%;
height: 100%;
object-fit: cover;
}
.cardInner {
position: relative;
z-index: 1;
}
.cardTitle {
font-size: var(--font-h1);
font-weight: var(--weight-medium);
letter-spacing: -0.04em;
line-height: 0.95;
margin: 0;
color: var(--color-white);
}
@media screen and (max-width: 767px) {
.wrap {
flex-flow: column;
}
.panel {
height: 30em;
}
.card {
padding: var(--space-l);
}
.cardTitle {
font-size: var(--font-h2);
}
}
Dependencies
gsap