How the Sticky Cards Section Works
The Sticky Cards section is built around a single GSAP concept: pin + scrub. The outer container is pinned to the viewport while the cards animate into and out of position as the user scrolls. Here's how that works in practice.
The pin
A ScrollTrigger pin keeps the section's wrapper fixed to the viewport for the duration of the scroll timeline:
ScrollTrigger.create({
trigger: wrapper,
pin: true,
start: 'top top',
end: () => `+=${cards.length * 100}vh`,
scrub: 1,
});
The end value is computed dynamically based on how many cards are in the section. Each card gets 100vh of scroll distance to work with — enough to feel deliberate without dragging.
Per-card timelines
Each card has its own GSAP timeline that plays within the scrubbed window. As the user scrolls into a card's range, the card lifts, the background color transitions, and the text resolves:
cards.forEach((card, i) => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: wrapper,
start: () => `top top-=${i * 100}vh`,
end: () => `top top-=${(i + 1) * 100}vh`,
scrub: 1,
},
});
tl.fromTo(card, { yPercent: 80, autoAlpha: 0 }, { yPercent: 0, autoAlpha: 1 });
tl.to(document.body, { backgroundColor: card.dataset.bg }, '<');
});
The < position parameter runs the background transition in parallel with the card lift — they arrive together, not sequentially.
Per-card color worlds
Each card defines its own background color via a data-bg attribute rendered from the section schema. The schema exposes a color picker per card in the Shopify theme editor. GSAP's to(document.body, { backgroundColor }) transition smooths between them as each card enters.
This is why the section feels like entering different rooms — not just swiping cards. The entire page's ambient color shifts.
The reduced-motion fallback
When prefers-reduced-motion: reduce is set, the scrub and pin are skipped entirely. The cards are unstacked and laid out vertically in their final state — full opacity, natural document flow. The per-card colors are applied as static CSS custom properties instead.
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) {
cards.forEach((card, i) => {
card.style.setProperty('--card-bg', card.dataset.bg);
card.style.opacity = '1';
card.style.transform = 'none';
});
return; // No ScrollTrigger registered
}
The layout is fully readable. Nothing is hidden. The content just isn't animated.
What this means for your theme
The section works best on full-width placements where nothing else on the page competes for vertical scroll distance. If your theme adds sticky headers or other pinned elements, test carefully — multiple simultaneous pin instances can interact in unexpected ways.
If you run into conflicts, the section exposes a Pin offset setting in the theme editor that adjusts start by a fixed pixel value.
