---
title: Scroll Spin
description: Rotates its children with the page scroll.
source: https://adamjurek.com/components/scroll-spin
---

## Home


Rotates whatever you put inside it as the page scrolls. Drag the viewport
buttons to see it at three widths - the amount of rotation per screen scrolled
is the same at every one.

<!-- ::start:showcase demo="scroll-spin" height="460" -->
<!-- ::end:showcase -->

The rotation is written straight onto the node in a `requestAnimationFrame`
callback, never through React state. At 60fps a state-driven version re-renders
the subtree on every frame of every scroll, which is the one thing guaranteed
to make a light page feel heavy.

> [!NOTE] Reduced motion
> Anyone with reduced motion set gets a still image. The effect never starts,
> rather than starting and being cancelled.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/scroll-spin.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/scroll-spin.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | motion · Scroll effects |
| Files | `scroll-spin.tsx`, `use-scroll-turn.ts` |
| Dependencies | None |
| Also installs | `use-scroll-turn` |
| Tags | scroll, transform, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry. This tab starts after
the install worked.

## Use it

```tsx
import { ScrollSpin } from "@sushindustries/ui";

export function Hero() {
	return (
		<ScrollSpin revolutions={2} tilt={8}>
			<img src="/mark.svg" alt="" />
		</ScrollSpin>
	);
}
```

The component emits class names from `@sushindustries/atoms`, so import that
stylesheet once at your root:

```ts
import "@sushindustries/atoms/atoms.css";
```


## Guides


## Composing it

`ScrollSpin` draws its own stage - a square box sized against its
container's width and the viewport height, centred, with the `perspective`
that makes the turn read as an object rather than a flat rotation. It needs a
parent with real width; inside something collapsed to zero (a flex child with
no basis) the square collapses with it.

## When not to use it

For anything that has to stay readable while it turns - body text, a form, a
diagram with labels around its edge. The rotation is the whole point for a
mark or a logo; it works against comprehension for anything a reader needs to
parse mid-turn.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - | Whatever should turn. A logo, a mark, an image, a diagram. |
| `revolutions?` | `number` | `2` | Viewport heights per full revolution. Higher is slower. Tied to viewport height rather than pixels so the rotation per "screen scrolled" is the same on a phone and on a monitor. |
| `tilt?` | `number` | `8` | Degrees of wobble on the X axis. Set to 0 for a flat turntable. |

<!-- /generated:api -->

### `revolutions`

Measured in viewport heights, not pixels. A pixel constant would mean a phone
user scrolling one screen sees three times the rotation a desktop user sees for
the same gesture.

### `tilt`

The wobble is a sine of the same scroll value, so it returns to zero at every
half turn rather than drifting. Set it to `0` for a turntable; anything above
about `12` starts to read as a wobble rather than a tilt.

> [!CAUTION] It writes to `style.transform`
> Do not also animate `transform` on the same element from CSS. The component
> overwrites the property on every frame, and your keyframes will silently lose.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="scroll-spin" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { ScrollSpin } from "@sushindustries/ui";

export function Hero() {
	return (
		<div className="hero-split">
			<ScrollSpin revolutions={3} tilt={6}>
				<img src="/mark.svg" alt="" width={240} height={240} />
			</ScrollSpin>
			<div>
				<h1 className="h1">Every visible element is a component.</h1>
			</div>
		</div>
	);
}
```

## What this example is not

The site's own hero. The real one turns a three.js model through the same
scroll measurement (`useScrollTurn`) rather than a flat image - `ScrollSpin`
is the CSS-only version, for a mark that's actually an image or SVG.
