Journal

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.