Critical dual-pinning layout conflict in `StickyStack` and reduced-motion content lockout in `HorizontalPan`

Author: codeCraft-RitikCreated Sep 12, 2026Updated Sep 12, 2026

Issue Title

[BUG] Critical dual-pinning layout conflict in StickyStack and reduced-motion content lockout in HorizontalPan


Description

In skills/taste-skill/SKILL.md (Sections 5.A and 5.B, lines 365–470), the canonical code skeletons for StickyStack and HorizontalPan contain critical layout, responsiveness, and accessibility (WCAG 2.1) bugs that cause jittery scrolling, coordinate drift, and complete content lockout for users with motion sensitivities.


Affected Files & Lines

  • File: skills/taste-skill/SKILL.md
  • Section 5.A: StickyStack canonical skeleton (lines 365–423)
  • Section 5.B: HorizontalPan canonical skeleton (lines 427–470)

Summary of Bugs

1. CSS position: sticky vs. GSAP pin: true Collision (StickyStack)

In StickyStack, the JSX elements have className="stack-card sticky top-0 ..." while the useEffect configures:

typescript
ScrollTrigger.create({
  trigger: card,
  start: "top top",
  endTrigger: cardEls[cardEls.length - 1],
  end: "top top",
  pin: true,
  pinSpacing: false,
});
  • The Problem: ScrollTrigger implements pinning by applying position: fixed. Mixing CSS position: sticky (sticky top-0) with ScrollTrigger's pin: true causes the browser's native compositor and GSAP's scroll engine to compete over the element's positioning on every scroll tick.
  • Official GSAP Guidance: GreenSock explicitly states: "Never apply CSS position: sticky to an element that is being pinned by ScrollTrigger."
  • Symptom: Severe scroll judder, visual snapping, and coordinate miscalculations during reverse scrolling.

2. Reduced-Motion Permanent Content Inaccessibility (HorizontalPan)

In HorizontalPan:

typescript
export function HorizontalPan({ children }: { children: React.ReactNode }) {
  const wrap = useRef<HTMLDivElement>(null);
  const track = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();

  useEffect(() => {
    if (reduce || !wrap.current || !track.current) return;
    // GSAP ScrollTrigger setup...
  }, [reduce]);

  return (
    <section ref={wrap} className="relative overflow-hidden">
      <div ref={track} className="flex h-[100dvh] items-center">
        {children}
      </div>
    </section>
  );
}
  • The Problem: When a user has prefers-reduced-motion: reduce enabled at the OS level, useReducedMotion() returns true and skips GSAP initialization. However, <section> unconditionally keeps overflow-hidden.
  • Symptom (WCAG 2.1 Failure): There is no scrollbar or CSS fallback. All cards/panels overflowing the initial screen width are permanently cut off and impossible to reach.

3. Stale Closure & Broken Resizing (HorizontalPan)

  • const distance = track.current!.scrollWidth - window.innerWidth; is evaluated only once on initial mount.
  • x: -distance and end: () => +=${distance} bind that static integer.
  • On window resize or font/asset load, invalidateOnRefresh: true does not recalculate the translation distance because x is a static primitive rather than a function callback.
  • On ultrawide displays where scrollWidth <= innerWidth, distance becomes negative, translating the track to the right into blank space.

Steps to Reproduce

  1. Copy StickyStack from skills/taste-skill/SKILL.md into any React / Next.js project.
  2. Render 4 full-screen cards inside StickyStack and scroll up and down in Chrome or Safari. Observe jitter and visual displacement when the browser's native sticky engine fights ScrollTrigger's fixed pin.
  3. Render HorizontalPan with multiple panels.
  4. In browser DevTools, emulate prefers-reduced-motion: reduce (Rendering -> Emulate CSS media feature prefers-reduced-motion).
  5. Attempt to view panel 2, 3, or 4. Notice they cannot be reached by keyboard, mouse wheel, or touch because overflow: hidden blocks all access while GSAP is disabled.

Proposed Fix / Drop-in Replacement

1. Fixed StickyStack Skeleton (Section 5.A)

Replace the StickyStack snippet in skills/taste-skill/SKILL.md with:

typescript
"use client";

import React, { useRef, useId } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";
import { useGSAP } from "@gsap/react";

if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger);
}

export function StickyStack({ cards }: { cards: React.ReactNode[] }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const reduceMotion = useReducedMotion();
  const instanceId = useId().replace(/:/g, "");
  const cardSelector = `.stack-card-${instanceId}`;

  useGSAP(
    () => {
      if (reduceMotion || !containerRef.current || cards.length <= 1) return;

      const cardElements = gsap.utils.toArray<HTMLElement>(
        cardSelector,
        containerRef.current
      );

      cardElements.forEach((card, index) => {
        if (index === cardElements.length - 1) return;
        const nextCard = cardElements[index + 1];

        // Pin each card except the last
        ScrollTrigger.create({
          trigger: card,
          start: "top top",
          endTrigger: cardElements[cardElements.length - 1],
          end: "top top",
          pin: true,
          pinSpacing: false,
          anticipatePin: 1,
          invalidateOnRefresh: true,
        });

        // Scale & fade previous card as next card enters
        gsap.to(card, {
          scale: 0.92,
          opacity: 0.55,
          ease: "power1.out",
          scrollTrigger: {
            trigger: nextCard,
            start: "top bottom",
            end: "top top",
            scrub: true,
            invalidateOnRefresh: true,
          },
        });
      });
    },
    { scope: containerRef, dependencies: [reduceMotion, cards.length] }
  );

  return (
    <div ref={containerRef} className="relative w-full">
      {cards.map((card, index) => (
        // FIXED: 'relative' instead of 'sticky top-0' avoids layout engine conflict
        <section
          key={index}
          style={{ zIndex: index + 1 }}
          className={`stack-card-${instanceId} relative flex min-h-[100dvh] w-full items-center justify-center will-change-transform`}
        >
          {card}
        </section>
      ))}
    </div>
  );
}

2. Fixed HorizontalPan Skeleton (Section 5.B)

Replace the HorizontalPan snippet in skills/taste-skill/SKILL.md with:

typescript
"use client";

import React, { useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useReducedMotion } from "motion/react";
import { useGSAP } from "@gsap/react";

if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger);
}

export function HorizontalPan({ children }: { children: React.ReactNode }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const trackRef = useRef<HTMLDivElement>(null);
  const reduceMotion = useReducedMotion();

  useGSAP(
    () => {
      if (reduceMotion || !containerRef.current || !trackRef.current) return;

      // Dynamic distance getter ensures responsive accuracy on resize
      const getDistance = (): number => {
        if (!trackRef.current) return 0;
        return Math.max(0, trackRef.current.scrollWidth - window.innerWidth);
      };

      if (getDistance() <= 0) return;

      gsap.to(trackRef.current, {
        x: () => -getDistance(),
        ease: "none",
        scrollTrigger: {
          trigger: containerRef.current,
          start: "top top",
          end: () => `+=${getDistance()}`,
          pin: true,
          scrub: 1,
          anticipatePin: 1,
          invalidateOnRefresh: true, // Dynamically calls distance function on resize
        },
      });
    },
    { scope: containerRef, dependencies: [reduceMotion] }
  );

  return (
    // FIXED: overflow-x-auto allows reduced-motion users to access all panels
    <section
      ref={containerRef}
      role="region"
      aria-label="Horizontal Content Track"
      className={`relative w-full ${
        reduceMotion
          ? "overflow-x-auto scroll-smooth focus:outline-none"
          : "overflow-hidden"
      }`}
      tabIndex={reduceMotion ? 0 : undefined}
    >
      <div
        ref={trackRef}
        className={`flex min-h-[100dvh] w-max items-center ${
          reduceMotion ? "snap-x snap-mandatory" : ""
        }`}
      >
        {children}
      </div>
    </section>
  );
}

Key Improvements in the Fix

Area Before After
StickyStack Layout sticky top-0 conflicts with pin: true relative + stacked zIndex gives ScrollTrigger sole ownership of fixed positioning.
Reduced-Motion Access Unconditional overflow-hidden blocks all content Uses overflow-x-auto when reduceMotion is true, keeping all content reachable.
Resize Responsiveness Static distance constant never updates Functional getter () => -getDistance() recalculates on resize with invalidateOnRefresh.
DOM Scoping Global gsap.utils.toArray(".stack-card") Instance-scoped selector via useId() and container scoping prevents multi-instance interference.