The section pins in place when scrolled to, the user's vertical scroll is read by ScrollTrigger and tweened into horizontal sliding of a track containing multiple slides — the industry-standard "pinned horizontal-scroll section" technique.
WHAT
Purpose and context
- Purpose: a section pins in place when scrolled to, vertical scroll is converted by GSAP ScrollTrigger into horizontal sliding of a multi-slide track; by default use the verbatim original at
Rheinmir/uiux-asset. - Trigger (when to use):
- A page needs a "horizontal gallery"-style section (case study, portfolio, product) while the user still just scrolls the mouse vertically as usual.
- User says "pin section", "horizontal scroll", "horizontal scroll section", "scroll-driven horizontal", "gsap scrolltrigger", or /gsap-scrolltrigger-pin.
- Non-goals: NOT for light scroll effects that don't need pinning (use
scroll-effects) or for smoothing the scroll feel of the whole page (uselenis-smooth-scroll) — those two skills do NOT pull in the GSAP dependency. Applies only to ONE specific pinned section.
Mental model
[data-hp-section] > .hp-pin (pin) > [data-hp-track] (tween xPercent, scrub) > [data-hp-slide] × N; end is the function that re-measures track.scrollWidth each refresh(); matchMedia chooses the GSAP branch or the reduce-motion branch (plain CSS horizontal scroll).
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | slide content of the section | yes | case study / portfolio / product |
| In | permission to pull external libs (GSAP) | yes | if not allowed to pull libs → use the trimmed version assets/, GSAP is still required |
| In | design.md of the project |
no | if present, change the color tokens to match it |
| Out | pinned section + horizontal track in the project | yes | copy the original file as-is (keep attribution comments) or adapt assets/ |
| Out | verification evidence playwright-verify |
yes | pins at the right time, no horizontal overflow, survives resize |
Rules and capabilities
- RULE-01 (MUST): The GSAP dependency is required — this skill does NOT run without GSAP + the ScrollTrigger plugin. Install in one of two ways:
- CDN (like demo.html):
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>+.../ScrollTrigger.min.js, load BEFORE the skill's script. - npm:
npm install gsap, thenimport gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; gsap.registerPlugin(ScrollTrigger);.
- CDN (like demo.html):
- RULE-02 (MUST):
prefers-reduced-motion: reduce→ the GSAP branch does NOT init anything at all (no ScrollTrigger, no pin, no scrub) — CSS automatically converts.hp-pinintooverflow-x: autoplain horizontal scroll (scroll-snap-type: x mandatory). Do not add any JS animation in this branch. - RULE-03 (MUST): Track uses
xPercent, do not usex(px) —xPercentautomatically follows the actual width of the track even when slide content changes size, no need to recalculate pixel offsets manually. - RULE-04 (MUST): Verify with
playwright-verify: scroll vertically through the entire section, assert the section pins at the right time (viewport does not scroll vertically further while the track still has slides), assert there is no horizontal overflow in the rest of the page, test resizing the viewport then scroll again to make sureScrollTrigger.refresh()has picked up the new layout. - Capabilities: read the original (web/git) + asset skill +
design.md; write HTML/CSS/JS into the target project; run a headless browser to verify.
Failure boundaries
- Missing GSAP + ScrollTrigger and not allowed to add a dependency → blocked (RULE-01), propose
scroll-effects/lenis-smooth-scrollif the requirement actually doesn't need pinning. - Open the file with
file://→ behavior is incorrect; must run locally over HTTP. - Assert verify is red (no pin, horizontal overflow, resize breaks) → failed, fix then verify again.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | requirement | Confirm pinning + horizontal scrolling is needed (not scroll-effects/lenis-smooth-scroll) |
select skill | no pinning needed → switch to another skill |
| W02 | effect | original Pen table | Open the link run, copy the entire file .html (+ _assets/) into the project, keep the attribution comment |
original file in the project | not allowed to pull in an external lib → B01 |
| W03 | effect | file + design.md |
Only edit content/images/colors | section has been adapted | — |
| W04 | deterministic | page running over HTTP | Verify RULE-04 with playwright-verify |
assert green | red → fix, repeat W04 |
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | the project is NOT allowed to pull in an external lib (original version) | use the vanilla condensed version assets/ (section Reference — Assets), read then ADAPT |
still needs GSAP (RULE-01) | W04 |
| B02 | conditional_required | prefers-reduced-motion: reduce |
no GSAP init, plain CSS horizontal scrolling (RULE-02) | — | W04 |
Validation and stopping
Deterministic assert at W04 (pins at the right time, no horizontal overflow, resize then scroll back still correct). Needs eyes: content/colors match the project. Stop when the assert is green; if it stays red after more than 2 loops → notify the user with a screenshot.
Examples
- Positive: "make a horizontal-scrolling portfolio section" → copy
horizontal-scroll-section-with-gsap-and-locomotive-scroll.htmlfromRheinmir/uiux-asset, change images/text → playwright scrolls vertically: section pins, track slides through all slides before releasing, no horizontal overflow. - Boundary/failure: use
x: -2000(px) instead ofxPercent→ resizing the viewport makes the track drift/miss the last slide → violates RULE-03, switch toxPercent: -100 * (slideCount - 1)then verify by resizing again.
Reference — Original — use FIRST (copy the file as-is, don't rewrite it)
The author's VERBATIM original code (public CodePen = MIT) is in an external repo Rheinmir/uiux-asset — live gallery: https://rheinmir.github.io/uiux-asset/scroll-effects/ .
Process: open the link run to see the exact effect → copy the file as-is .html (+ the folder _assets/ if the file references it) into the project → keep the attribution comment on the first line → only change content/images/colors. Run locally over HTTP, not file://.
git clone --depth 1 https://github.com/Rheinmir/uiux-asset if you need the whole offline bundle.
| Original Pen — author | Link |
|---|---|
| Horizontal Scroll Section with GSAP and Locomotive Scroll — cameronknight | run · code |
| Smooth Scroll Stacking Accordion — supah | run · code |
| Infinite Horizontal Scroll with Progress Tracking — haptichash | run · code |
| Pinned Split-Screen Mask Reveal — gridmorphic | run · code |
| Scroll-Driven SVG Map Editorial Gallery — knyttneve | run · code |
assets/ in this skill is just a self-written vanilla condensed version — use it when the project is NOT allowed to pull external libs; by default use the original above.
Reference — Assets
assets/demo.html + assets/gsap-scrolltrigger-pin.css + assets/gsap-scrolltrigger-pin.js — read then ADAPT (change slide content, color tokens per the project's design.md if any). Open demo.html directly in a browser to see the mechanism running for real (the asset loads GSAP via CDN).
Reference — Mechanism
- Markup:
[data-hp-section] > .hp-pin > [data-hp-track] > [data-hp-slide] × N— the track is a flex-row, each slideflex: 0 0 100vw, total width = N × 100vw (naturally from layout, not computed by hand). ScrollTriggerpin.hp-pin(pin: true,start: "top top"),scrub: 1locks the animation progress to the exact scroll position — it's not a time-based animation.- The animation is
gsap.to(track, { xPercent: -100 * (slideCount - 1) })—slideCountread fromtrack.querySelectorAll('[data-hp-slide]').length, not hardcoding the slide count in the code. endis a FUNCTION:() => "+=" + (track.scrollWidth - window.innerWidth)— recomputed every timeScrollTrigger.refresh()runs (re-measured from the DOM's actualtrack.scrollWidthat that moment), not a fixed number.ScrollTrigger.matchMedia()wrap the entire init: when the viewport changes breakpoint orprefers-reduced-motionchanges, GSAP automatically reverts the old branch and reruns the matching branch — this is the standard way to "survive" resizes, instead of writing a resize listener by hand.window.addEventListener('load', () => ScrollTrigger.refresh())handle the case where images/fonts finish loading only then doesscrollWidthof the track change.
Origin
Batch 180926 — the VERBATIM original is at Rheinmir/uiux-asset/scroll-effects/ (see the "Original" section above); assets/ is a shortened vanilla version written by hand.