useScrollTurn

@sushindustries/ui

Scroll position as a rotation, delivered once per frame, written straight to wherever it goes.

3 min read
View as Markdown

The measurement behind ScrollSpin, on its own, because the same numbers have to drive two things that share no code.

Viewport width
Desktop100%whatever the page has
tanstacktanstack add https://adamjurek.com/r/tanstack/use-scroll-turn.jsonshadcnpnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/use-scroll-turn.json

Never through state

useScrollTurn(({ turn, wobble }) => {
	node.style.transform = `rotateY(${turn * 360}deg)`;
});
tsx

The callback runs inside requestAnimationFrame and is expected to write somewhere directly. Nothing here holds state, because at 60fps a state-driven version re-renders its subtree on every frame of every scroll, which is the one reliable way to make a light page feel heavy.

The same reasoning is why ProductViewer takes a modelRef rather than a rotation prop: a prop would re-render the whole viewer sixty times a second to change one float React has no reason to know about.

Wrap the callback in `useCallback`

It is a dependency of the effect. An inline arrow function is a new function on every render, so the listener is torn down and rebuilt every time anything above it changes.

Where this is used

WhereWrites to
ScrollSpina CSS transform on a wrapper element
apps/web/src/modules/chrome/logo-model.tsxrotation.y on the hero's GLB

Install

tanstack add https://adamjurek.com/r/tanstack/use-scroll-turn.json
shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/use-scroll-turn.json
shell
pnpm add @sushindustries/ui @sushindustries/atoms
shell

What you get

Version0.1.0
Categorymotion · Scroll effects
Filesuse-scroll-turn.ts
DependenciesNone
Tagsscroll, hook, no-deps

No runtime dependencies

It brings nothing with it beyond the stylesheet.

Used byScroll SpinScroll SpinRotates its children with the page scroll, in a rAF callback rather than React state.@sushindustries/ui · motion
Mentioned inMotion and depth