Back to skills
SKILL.md
R3f Scroll Driven 3d
ASecurityConnect GSAP ScrollTrigger to React Three Fiber — Zustand bridge, useFrame animation, scroll progress to 3D transforms. The pattern for scroll-driven 3D product showcases.
- 3 stars
- 0 votes
- 0 copies
- 0 views
- Added October 16, 2026
Security analysis
100/100npx -y skills add RaNDoM6913/claude-code-superkit --skill r3f-scroll-driven-3d --agent claude-codeAre you the author of R3f Scroll Driven 3d?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/random6913-r3f-scroll-driven-3d)---
name: r3f-scroll-driven-3d
description: Connect GSAP ScrollTrigger to React Three Fiber — Zustand bridge, useFrame animation, scroll progress to 3D transforms. The pattern for scroll-driven 3D product showcases.
user-invocable: false
---
# Scroll-Driven 3D with GSAP + R3F
Architecture pattern for connecting GSAP ScrollTrigger to React Three Fiber scenes via Zustand store.
## Why This Pattern?
**Problem:** GSAP runs in the DOM. R3F runs in WebGL. They can't communicate directly.
**Solution:** Zustand store as a bridge — GSAP writes scroll progress, R3F reads it in useFrame.
```
[GSAP ScrollTrigger] → writes → [Zustand Store] → reads → [R3F useFrame]
(DOM) (shared state) (WebGL)
```
## Why Zustand (Not React State/Context)?
- React state/context triggers re-renders on every scroll tick (60fps = 60 re-renders/sec)
- Zustand `getState()` reads directly without subscribing — zero re-renders
- useFrame already runs at 60fps — just read the latest value
## Implementation
### Step 1: Create the store
```tsx
// stores/useScrollStore.ts
import { create } from 'zustand';
interface ScrollStore {
progress: number; // 0-1 scroll progress
currentTexture: string; // active screen texture path
setProgress: (p: number) => void;
setTexture: (t: string) => void;
}
export const useScrollStore = create<ScrollStore>((set) => ({
progress: 0,
currentTexture: '/textures/screen-1.png',
setProgress: (p) => set({ progress: p }),
setTexture: (t) => set({ currentTexture: t }),
}));
```
### Step 2: GSAP writes to store
```tsx
// components/ScrollSection.tsx
import { useRef, useEffect } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useScrollStore } from '@/stores/useScrollStore';
gsap.registerPlugin(ScrollTrigger);
export function ScrollSection() {
const ref = useRef<HTMLDivElement>(null);
const { setProgress, setTexture } = useScrollStore();
useEffect(() => {
const ctx = gsap.context(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ref.current,
start: 'top top',
end: 'bottom bottom',
scrub: 1, // number, NOT true
invalidateOnRefresh: true, // MUST have
pin: true,
},
});
// Animate progress 0 → 1
tl.to({}, {
duration: 1,
onUpdate: function() {
setProgress(this.progress());
},
});
// Swap texture at 50% scroll
tl.call(() => setTexture('/textures/screen-2.png'), [], 0.5);
// CRITICAL: extend timeline to full duration
tl.set({}, {}, 1.0);
}, ref);
return () => ctx.revert();
}, []);
return <div ref={ref} style={{ height: '300vh' }} />;
}
```
### Step 3: R3F reads in useFrame
```tsx
// components/3d/PhoneModel.tsx
import { useFrame } from '@react-three/fiber';
import { useScrollStore } from '@/stores/useScrollStore';
import { useRef } from 'react';
import * as THREE from 'three';
export function PhoneModel() {
const groupRef = useRef<THREE.Group>(null);
useFrame(() => {
if (!groupRef.current) return;
// CRITICAL: getState() not hook — no re-renders
const { progress } = useScrollStore.getState();
// Rotate based on scroll
groupRef.current.rotation.y = progress * Math.PI * 2;
// Phase-based animation
if (progress < 0.3) {
// Entry: scale up
const t = progress / 0.3;
groupRef.current.scale.setScalar(0.5 + t * 0.5);
} else if (progress < 0.7) {
// Showcase: full size, rotate
groupRef.current.scale.setScalar(1);
} else {
// Exit: scale down
const t = (progress - 0.7) / 0.3;
groupRef.current.scale.setScalar(1 - t * 0.3);
}
});
return (
<group ref={groupRef}>
{/* Your 3D model here */}
</group>
);
}
```
## Performance Tips
- **Pre-allocate:** No `new THREE.Vector3()` inside useFrame — use refs
- **getState():** Always in useFrame, never hook subscription
- **DPR:** `dpr={[1, 2]}` — higher wastes mobile GPU
- **Dispose:** `dispose={null}` on reusable groups
## Dynamic Texture Swapping
```tsx
useFrame(() => {
const { currentTexture } = useScrollStore.getState();
// Load and apply texture based on store value
});
```
Use `tl.call(() => setTexture('/new.png'), [], timePosition)` in GSAP timeline to trigger swaps at specific scroll positions.
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.