{"id":13998515,"url":"https://github.com/14islands/r3f-scroll-rig","last_synced_at":"2025-10-18T23:06:45.295Z","repository":{"id":65728284,"uuid":"275910128","full_name":"14islands/r3f-scroll-rig","owner":"14islands","description":"A react-three-fiber scroll-rig for syncing 3D meshes and DOM elements.","archived":false,"fork":false,"pushed_at":"2025-01-10T09:02:09.000Z","size":14218,"stargazers_count":799,"open_issues_count":3,"forks_count":42,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-04-13T12:41:32.041Z","etag":null,"topics":["lenis","react-three-fiber","smoothscroll"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/14islands.png","metadata":{"files":{"readme":"README.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-29T19:41:37.000Z","updated_at":"2025-04-08T15:54:40.000Z","dependencies_parsed_at":"2025-04-06T07:28:53.390Z","dependency_job_id":"3714ca31-997e-44c8-a9e7-130f4de4532c","html_url":"https://github.com/14islands/r3f-scroll-rig","commit_stats":{"total_commits":496,"total_committers":6,"mean_commits":82.66666666666667,"dds":"0.014112903225806495","last_synced_commit":"8b8dc9205856c421bc3251eb6f3572b60b5f0582"},"previous_names":[],"tags_count":85,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/14islands%2Fr3f-scroll-rig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/14islands%2Fr3f-scroll-rig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/14islands%2Fr3f-scroll-rig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/14islands%2Fr3f-scroll-rig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/14islands","download_url":"https://codeload.github.com/14islands/r3f-scroll-rig/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254140745,"owners_count":22021219,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lenis","react-three-fiber","smoothscroll"],"created_at":"2024-08-09T19:01:44.284Z","updated_at":"2025-10-18T23:06:40.257Z","avatar_url":"https://github.com/14islands.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# @14islands/r3f-scroll-rig\n\n![npm](https://img.shields.io/npm/v/@14islands/r3f-scroll-rig?color=magenta\u0026style=flat-square)\n\nProgressively enhance a React website with WebGL using `@react-three/fiber` and smooth scrolling.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg width=\"49.5%\" src=\"https://www.dropbox.com/s/kqaweg996jtb2ho/14islands600_10fps_2.gif?dl=0\u0026raw=1\" style=\"float:left\" /\u003e\n  \u003cimg width=\"49.5%\" src=\"https://www.dropbox.com/s/vmpqf17oy0otkkl/pluto600_10fps.gif?dl=0\u0026raw=1\" style=\"float:right\" /\u003e\n\u003c/p\u003e\n\n[ \u003ca href=\"#features-\"\u003eFeatures\u003c/a\u003e |\n\u003ca href=\"#introduction-\"\u003eIntroduction\u003c/a\u003e |\n\u003ca href=\"#installing-\"\u003eInstalling\u003c/a\u003e |\n\u003ca href=\"#getting-started-\"\u003eGetting Started\u003c/a\u003e |\n\u003ca href=\"#examples-\"\u003eExamples\u003c/a\u003e |\n\u003ca href=\"#api-%EF%B8%8F\"\u003eAPI\u003c/a\u003e |\n\u003ca href=\"#gotchas-\"\u003eGotchas\u003c/a\u003e ]\n\n# Features 🌈\n\n- 🔍 Tracks DOM elements and draws Three.js objects in their place using correct scale and position.\n- 🤷 Framework agnostic - works with `next.js`, `gatsby.js`, `create-react-app` etc.\n- 📐 Can render objects in viewports. Makes it possible for each object to have a unique camera, lights, environment map, etc.\n- 🌠 Helps load responsive images from the DOM. Supports `\u003cpicture\u003e`, `srset` and `loading=\"lazy\"`\n- 🚀 Optimized for performance. Calls `getBoundingClientRect()` once on mount, and uses IntersectionObserver/ResizeObserver to keep track of elements.\n- 🧈 Uses [Lenis](https://github.com/darkroomengineering/lenis) for accessible smooth scrolling\n- ♻️ 100% compatible with the @react-three ecosystem, like [Drei](https://github.com/pmndrs/drei), [react-spring](https://www.react-spring.dev/) and [react-xr](https://github.com/pmndrs/react-xr)\n\n# Introduction 📚\n\nMixing WebGL with scrolling HTML is hard. One way is to have multiple canvases, but there is a browser-specific limit to how many WebGL contexts can be active at any one time, and resources can't be shared between contexts.\n\n\u003cimg align=\"right\" width=\"40%\" src=\"https://user-images.githubusercontent.com/420472/191715313-cc813f47-4e4a-454f-a2f5-d8e2ec998c95.jpeg\" /\u003e\n\nThe scroll-rig has only one shared `\u003cGlobalCanvas/\u003e` that stays in between page loads.\n\nReact DOM components can choose to draw things on this canvas while they are mounted using a custom hook called `useCanvas()` or the `\u003cUseCanvas/\u003e` tunnel component.\n\nThe library also provides means to sync WebGL objects with the DOM while scrolling. We use a technique that tracks “proxy” elements in the normal page flow and updates the WebGL scene positions to match them.\n\nThe `\u003cScrollScene/\u003e`, `\u003cViewportScrollScene/\u003e` or the underlying `useTracker()` hook will detect initial location and dimensions of the proxy elements, and update positions while scrolling.\n\nEverything is synchronized in lockstep with the scrollbar position on the main thread.\n\nFurther reading: [Progressive Enhancement with WebGL and React](https://medium.com/14islands/progressive-enhancement-with-webgl-and-react-71cd19e66d4)\n\n# Installing 💾\n\n`yarn add @14islands/r3f-scroll-rig @react-three/fiber three`\n\n# Getting Started 🛫\n\n1. Add `\u003cGlobalCanvas\u003e` to your layout. Keep it outside of your router to keep it from unmounting when navigating between pages.\n\n2. Add `\u003cSmoothScrollbar/\u003e` to your layout. In order to perfectly match WebGL objects and DOM content, the browser scroll position needs to be animated on the main thread.\n\n\u003cdetails open\u003e\n\u003csummary\u003eNext.js\u003c/summary\u003e\n\n```jsx\nimport { GlobalCanvas, SmoothScrollbar } from '@14islands/r3f-scroll-rig'\n\n// _app.jsx\nfunction App({ Component, pageProps }: AppProps) {\n  return (\n    \u003c\u003e\n      \u003cGlobalCanvas /\u003e\n      \u003cSmoothScrollbar /\u003e\n      \u003cComponent {...pageProps} /\u003e\n    \u003c/\u003e\n  )\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eGatsby.js\u003c/summary\u003e\n\n```jsx\n// gatsby-browser.js\nimport { GlobalCanvas, SmoothScrollbar } from '@14islands/r3f-scroll-rig'\n\nexport const wrapRootElement = ({ element }) =\u003e (\n  \u003c\u003e\n    \u003cGlobalCanvas /\u003e\n    \u003cSmoothScrollbar /\u003e\n    {element}\n  \u003c/\u003e\n)\n```\n\n\u003c/details\u003e\n\n2. Track a DOM element and render a Three.js object in its place\n\nThis is a basic example of a component that tracks the DOM and use the canvas to render a Mesh in its place:\n\n```jsx\nimport { UseCanvas, ScrollScene } from '@14islands/r3f-scroll-rig'\n\nexport const HtmlComponent = () =\u003e (\n  const el = useRef()\n  return (\n    \u003c\u003e\n      \u003cdiv ref={el}\u003eTrack me!\u003c/div\u003e\n      \u003cUseCanvas\u003e\n        \u003cScrollScene track={el}\u003e\n          {(props) =\u003e (\n            \u003cmesh {...props}\u003e\n              \u003cplaneGeometry /\u003e\n              \u003cmeshBasicMaterial color=\"turquoise\" /\u003e\n            \u003c/mesh\u003e\n          )}\n        \u003c/ScrollScene\u003e\n      \u003c/UseCanvas\u003e\n    \u003c/\u003e\n  )\n)\n```\n\n## How it works:\n\n- The page layout is styled using normal HTML \u0026 CSS\n- The `UseCanvas` component is used to send its children to the `GlobalCanvas` while the component is mounted\n- A `\u003cScrollscene\u003e` is used to track the DOM element\n- Inside the `\u003cScrollScene\u003e` we place a mesh which will receive the correct scale as part of the passed down `props`\n\n**⚠️ Note:** HMR might not work for the children of `\u003cUseCanvas\u003e` unless you defined them outside. Also, the props on the children are not reactive by default since the component is tunneled to the global canvas. \u003ca href=\"/docs/api.md#usecanvas\"\u003eUpdated props need to be tunneled like this.\u003c/a\u003e\n\nLearn more about edge cases and solutions in the \u003ca href=\"#gotchas-\"\u003egotchas section\u003c/a\u003e.\n\n# Examples 🎪\n\n- [ScrollScene basic example](https://codesandbox.io/s/hello-world-ibc8y7?file=/src/App.jsx)\n- [ScrollScene with GLB model \u0026 events from both DOM \u0026 Canvas](https://codesandbox.io/s/scrollscene-with-glb-6l2fc3?file=/src/App.js)\n- [ViewportScrollScene with custom camera and controls](https://codesandbox.io/s/hello-viewportscrollscene-fu0ky6?file=/src/App.jsx)\n- [Loading textures from \u0026lt;img\u0026gt; tags](https://codesandbox.io/s/load-image-from-dom-n120ll?file=/src/App.jsx)\n- [Load responsive texture from the DOM](https://codesandbox.io/s/load-responsive-picture-from-dom-rgcx4b?file=/src/App.jsx)\n- [HTML parallax with useTracker() and Framer Motion](https://codesandbox.io/s/parallax-with-framer-motion-dx2v1p?file=/src/App.jsx)\n- [A sticky ScrollScene from the powerups samples](https://codesandbox.io/s/r3f-scroll-rig-sticky-box-w5v4u7?file=/src/App.jsx)\n- [A basic Post Processing example](https://codesandbox.io/p/sandbox/hello-scrollscene-forked-cp3n93?file=/src/App.jsx)\n\n# API ⚙️\n\nAll components \u0026 hooks are described in the [API docs](/docs/api.md)\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd valign=\"top\"\u003e\n      \u003ch2\u003e\u003ca href=\"/docs/api.md#components\"\u003eComponents\u003c/a\u003e\u003c/h2\u003e\n        \u003cul\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#globalcanvas\"\u003eGlobalCanvas\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#smoothscrollbar\"\u003eSmoothScrollbar\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#usecanvas\"\u003eUseCanvas\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#scrollscene\"\u003eScrollScene\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#viewportscrollscene\"\u003eViewportScrollScene\u003c/a\u003e\u003c/li\u003e\n        \u003c/ul\u003e\n       \u003ctd valign=\"top\"\u003e\n       \u003ch2\u003e \u003ca href=\"/docs/api.md#hooks\"\u003eHooks\u003c/a\u003e\u003c/h2\u003e\n        \u003cul\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#usescrollrig\"\u003euseScrollRig\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#usescrollbar\"\u003euseScrollbar\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#usetracker\"\u003euseTracker\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#usecanvas-1\"\u003euseCanvas\u003c/a\u003e\u003c/li\u003e\n          \u003cli\u003e\u003ca href=\"/docs/api.md#useimageastexture\"\u003euseImageAsTexture\u003c/a\u003e\u003c/li\u003e\n        \u003c/ul\u003e\n    \u003c/td\u003e\n\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n# Gotchas 🧐\n\n\u003cdetails\u003e\n  \u003csummary\u003eThe default camera\u003c/summary\u003e\n\nThe default scroll-rig camera is locked to a 50 degree Field-of-View.\n\nIn order to perfectly match DOM dimensions, the camera distance will be calculated. This calculation is based on screen height since Threejs uses a vertical FoV. This means the camera position-z will change slightly based on your height.\n\nYou can override the default camera behaviour, and for instance set the distance and have a variable FoV instead:\n\n```jsx\n\u003cGlobalCanvas camera={{ position: [0, 0, 10] }} /\u003e\n```\n\nOr change the FoV, which would move the camera further away in this case:\n\n```jsx\n\u003cGlobalCanvas camera={{ fov: 20 }} /\u003e\n```\n\nIf you need full control of the camera you can pass in a custom camera as a child instead.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eUse relative scaling\u003c/summary\u003e\n  Always base your sizes on the `scale` passed down from ScrollScene/ViewportScrollScene/useTracker in order to have consistent scaling for all screen sizes.\n\nThe `scale` is always matching the tracked DOM element and will update based on media queries etc.\n\n```jsx\n\u003cScrollScene track={el}\u003e\n  {{ scale }} =\u003e (\n  \u003cmesh scale={scale} /\u003e\n  )}\n\u003c/ScrollScene\u003e\n```\n\nScale is a 3-dimensional vector type from [vecn](https://www.npmjs.com/package/vecn) that support swizzling and object notation. You can do things like:\n\n```js\nposition.x === position[0]\nposition.xy =\u003e [x,y]\nscale.xy.min() =\u003e Math.min(scale.x, scale.y)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eZ-Fighting on 3D objects (scaleMultiplier)\u003c/summary\u003e\n\nBy default the scroll-rig will calculate the camera FoV so that 1 pixel = 1 viewport unit.\n\nIn some cases, this can mess up the depth sorting, leading to visual glitches in a 3D model. A 1000 pixel wide screen would make the scene 1000 viewport units wide, and by default the camera will also be positioned ~1000 units away in Z-axis (depending on the FoV and screen hight).\n\nOne way to fix this is to enable the [logarithmicDepthBuffer](https://threejs.org/docs/index.html?q=webglre#api/en/renderers/WebGLRenderer) but that can be bad for performance.\n\nA better way to fix the issue is to change the GlobalCanvas `scaleMultiplier` to something like `0.01` which would make 1000px = 10 viewport units.\n\n```jsx\n\u003cGlobalCanvas scaleMultiplier={0.01} /\u003e\n```\n\nThe `scaleMultiplier` setting updates all internal camera and scaling logic. Hardcoded scales and positions would need to be updated if you change this setting.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eMatching exact hex colors\u003c/summary\u003e\n\nBy default R3F uses ACES Filmic tone mapping which makes 3D scenes look great.\n\nHowever, if you need to match hex colors or show editorial images, you can disable it per material like so:\n\n```jsx\n\u003cmeshBasicMaterial toneMapping={false} /\u003e\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eCumulative layout shift (CLS)\u003c/summary\u003e\n\nAll items on the page should have a predictable height - always define an aspect ratio using CSS for images and other interactive elements that might impact the document height as they load.\n\nThe scroll-rig uses `ResizeObserver` to detect changes to the `document.body` height, for instance after webfonts loaded, and will automatically recalculate postions.\n\nIf this fails for some reason, you can trigger a manual `reflow()` to recalculate all cached positions.\n\n```jsx\nconst { reflow } = useScrollRig()\n\nuseEffect(() =\u003e {\n  heightChanged \u0026\u0026 reflow()\n}, [heightChanged])\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003ePerformance tips\u003c/summary\u003e\n\n- Use CSS animations whenever possible instead of JS for maximum smoothness\n- Consider disabling SmoothScrollbar and all scrolling WebGL elements on mobile - it is usually laggy.\n- Make sure you [read, understand and follow all performance recomendations](https://docs.pmnd.rs/react-three-fiber/advanced/pitfalls) associated with `React` and `three`:\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eHow to catch events from both DOM and Canvas\u003c/summary\u003e\n\nThis is possible in R3F by re-attaching the event system to a parent of the canvas:\n\n```tsx\nconst ref = useRef()\nreturn (\n  \u003cdiv ref={ref}\u003e\n    \u003cGlobalCanvas\n      eventSource={ref} // rebind event source to a parent DOM element\n      eventPrefix=\"client\" // use clientX/Y for a scrolling page\n      style={{\n        pointerEvents: 'none', // delegate events to wrapper\n      }}\n    /\u003e\n  \u003c/div\u003e\n)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eCan I use R3F events in `ViewportScrollScene`?\u003c/summary\u003e\n\nYes, events will be correctly tunneled into the viewport, if you follow the steps above to re-attach the event system to a parent of the canvas.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003einViewportMargin is not working in CodeSandbox\u003c/summary\u003e\n\nThe CodeSandbox editor runs in an iframe which breaks the IntersectionObserver's `rootMargin`. If you open the example outside the iframe, you'll see it's working as intended.\n\nThis is know [issue](https://github.com/thebuilder/react-intersection-observer/issues/330#issuecomment-612221114).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eHMR is not working with UseCanvas children\u003c/summary\u003e\n\nThis is a known issue with the `UseCanvas` component.\n\nYou can either use the `useCanvas()` hook instead, or make HMR work again by defining your children as top level functions instead of inlining them:\n\n```jsx\n// HMR will work on me since I'm defined here!\nconst MyScrollScene = ({ el }) =\u003e \u003cScrollScene track={el}\u003e/* ... */\u003c/ScrollScene\u003e\n\nfunction MyHtmlComponent() {\n  return (\n    \u003cUseCanvas\u003e\n      \u003cMyScrollScene /\u003e\n    \u003c/UseCanvas\u003e\n  )\n}\n```\n\nA similar [issue](https://github.com/pmndrs/tunnel-rat/issues/4) exist in `tunnel-rat`.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eGlobal render loop\u003c/summary\u003e\n\nThe scroll-rig runs a custom render loop of the global scene inside r3f. It runs with priority `1000`.\n\nYou can disable the global render loop using `globalRender` or change the priority with the `globalPriority` props on the `\u003cGlobalCanvas\u003e`. You can still schedule your own render passes before or after the global pass using `useFrame` with your custom priority.\n\nThe main reason for running our own custom render pass instead of the default R3F render, is to be able to avoid rendering when no meshes are in the viewport. To enable this you need to set `frameloop=\"demand\"` on the GlobalCanvas.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eAdvanced - run frameloop on demand\u003c/summary\u003e\n\nIf the R3F frameloop is set to `demand` - the scroll rig will make sure global renders and viewport renders only happens if it's needed.\n\nTo request global render call `requestRender()` from `useScrollRig` on each frame. `ScrollScene` will do this for you when the mesh is in viewport.\n\nThis library also supports rendering separate scenes in viewports as a separate render pass by calling `renderViewport()`. This way we can render scenes with separate lights or different camera than the global scene. This is how `ViewportScrollScene` works.\n\nIn this scenario you also need to call `invalidate` to trigger a new R3F frame.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eHow to use post-processing\u003c/summary\u003e\n\nPost processing runs in a separate pass so you need to manually disable the global render loop to avoid double renders.\n\n```jsx\n\u003cGlobalCanvas globalRender={false} scaleMultiplier={0.01}\u003e\n  \u003cEffects /\u003e\n\u003c/GlobalCanvas\u003e\n```\n\nNote: `ViewportScrollScene` will not be affected by global postprocessing effects since it runs in a separate render pass.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eHow can I wrap my UseCanvas meshes in a shared Suspense?\u003c/summary\u003e\n\nPlease read the API docs on using [children as a render function](/docs/api.md#children-as-render-function) for an example.\n\n\u003c/details\u003e\n\n# In the wild 🐾\n\n- [14islands.com](https://14islands.com) by [14islands](https://14islands.com)\n- [v3.14islands.com](https://v3.14islands.com) by [14islands](https://14islands.com)\n- [Pluto.app](https://pluto-xr.netlify.app/) by [14islands](https://14islands.com)\n- [Myriad.video](https://myriad.video/) by [14islands](https://14islands.com)\n- [Neko Health](https://www.nekohealth.com/) by [14islands](https://14islands.com)\n- ~~[Playgoals.com](https://playgoals.com/) by [14islands](https://14islands.com)~~\n- [Goals studio](https://studio.playgoals.com/) by [14islands](https://14islands.com)\n- ~~[Pluto dev portal](https://dev.pluto.app/) by [14islands](https://14islands.com)~~\n- [Quantum Wallet](https://quantumwallet.tech/) by [14islands](https://14islands.com)\n- [Metamask Learn](https://learn.metamask.io/) by [Antinomy Studio](https://antinomy.studio)\n- [Lynxeye](https://lynxeye.com/) by [14islands](https://14islands.com)\n- [Astra Nova](https://astranova.world/) by [estudio/nk](https://thefwa.com/profiles/estudionk-r) and [@juliperoncini](https://twitter.com/juliperoncini)\n- [Axolot Games](https://www.axolotgames.com/) by [14islands](https://14islands.com)\n- [Cartier 365](https://365ayearof.cartier.com/en/) by [14islands](https://14islands.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F14islands%2Fr3f-scroll-rig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F14islands%2Fr3f-scroll-rig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F14islands%2Fr3f-scroll-rig/lists"}