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:
StickyStackcanonical skeleton (lines 365–423) - Section 5.B:
HorizontalPancanonical 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:
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 CSSposition: sticky(sticky top-0) with ScrollTrigger'spin: truecauses 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:
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: reduceenabled at the OS level,useReducedMotion()returnstrueand skips GSAP initialization. However,<section>unconditionally keepsoverflow-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: -distanceandend: () => +=${distance}bind that static integer.- On window resize or font/asset load,
invalidateOnRefresh: truedoes not recalculate the translation distance becausexis a static primitive rather than a function callback. - On ultrawide displays where
scrollWidth <= innerWidth,distancebecomes negative, translating the track to the right into blank space.
Steps to Reproduce
- Copy
StickyStackfromskills/taste-skill/SKILL.mdinto any React / Next.js project. - Render 4 full-screen cards inside
StickyStackand 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. - Render
HorizontalPanwith multiple panels. - In browser DevTools, emulate
prefers-reduced-motion: reduce(Rendering->Emulate CSS media feature prefers-reduced-motion). - Attempt to view panel 2, 3, or 4. Notice they cannot be reached by keyboard, mouse wheel, or touch because
overflow: hiddenblocks 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:
"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:
"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. |
Source: Leonxlnx/taste-skill