# Strata — Scroll Animation for React

> A pinned scroll section that scrubs an image sequence like a product turntable — pass your own frames or get a seeded faceted gem rendered and cached on the fly, loaded coarse-to-fine behind a hairline progress bar, with neighbour crossfades and captions that fade in at their progress points.

- Page: https://crucibleui.com/components/scroll/strata
- Category: [scroll](https://crucibleui.com/components/scroll)
- Tier: pro (one-time purchase, see https://crucibleui.com/pricing)
- Engine: canvas

## Install

```bash
npx shadcn@latest add @crucible/strata
```

Pro install requires adding the token-bearing `@crucible` registry to components.json (issued at https://crucibleui.com/account after purchase).

Import from `@/components/crucible/strata`.

## Props (`StrataProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `frames?` | `string[]` | — | Image sequence to scrub, in playback order (URLs). Frames load progressively coarse-to-fine, so scrubbing works long before the last one lands. When omitted, a seeded procedural turntable renders instead: a faceted gem rotating a full 360°, generated lazily and cached as bitmaps. |
| `pinLength?` | `number` | `300` | Section height as a percentage of the viewport (or the `scroller`'s height). The extra length beyond 100 is the pinned scroll distance that scrubs the sequence. |
| `captions?` | `StrataCaption[]` | `[]` | Overlay captions that fade in and out at their progress points. |
| `scroller?` | `React.RefObject<HTMLElement \| null>` | `the viewport` | Scroll container to track instead of the window. Pass a ref to an `overflow-y-auto` ancestor; the pin then sizes itself to THAT box. |
| `fit?` | `"contain" \| "cover"` | `"contain"` | How frames fit the stage. |
| `crossfade?` | `boolean` | `true` | Blend between neighbouring frames for sub-frame smoothness. |
| `smoothing?` | `number` | `0.5` | Scrub catch-up, 0–1. 0 locks frames 1:1 to the scroll position; higher values ease the turn into place. |
| `frameCount?` | `number` | `60` | Frames in the procedural sequence (ignored when `frames` is set). |
| `seed?` | `number` | `7` | Seed for the procedural gem's cut (facet count, proportions, sparkle). |
| `colors?` | `[string, string]` | `["#e2e8f0", "#93c5fd"]` | Procedural gem palette as hex: `[body, accent]` — the crystal's tint and the cool rim light that catches its back facets. |
| `hud?` | `boolean` | `true` | Show the small turn readout (degrees, or frame number for custom frames). |
| `idle?` | `boolean` | `true` | Gentle hover-bob on the procedural gem so the stage never looks frozen. Ignored for custom `frames`. |
| `label?` | `string` | — | Accessible description of the sequence. When set the canvas is exposed as `role="img"` with this label; otherwise it is treated as decorative. |
| `dpr?` | `number` | `2` | Max devicePixelRatio for the stage canvas. |

Also accepts: `Omit< React.ComponentPropsWithoutRef<"section">, "children" >`.

## Usage example

```tsx
"use client";

import * as React from "react";

import {
  Strata,
  type StrataCaption,
} from "@/components/crucible/strata";

const CAPTIONS: StrataCaption[] = [
  { at: 0, text: "Scroll to turn it." },
  { at: 0.3, text: "Each step of the scroll advances one frame." },
  { at: 0.62, text: "Captions fade in at their progress points." },
  { at: 0.88, text: "Scroll back up to turn it back." },
];

export default function StrataDemo() {
  const scrollRef = React.useRef<HTMLDivElement>(null);

  return (
    <div
      ref={scrollRef}
      className="relative isolate h-[520px] w-full overflow-y-auto overflow-x-hidden rounded-xl bg-neutral-950 [scrollbar-width:thin]"
    >
      <Strata scroller={scrollRef} captions={CAPTIONS} />
      <div className="flex h-[220px] items-center justify-center px-6 text-center">
        <p className="max-w-xs text-sm text-white/45">
          One full turn. Scroll back up to rewind it.
        </p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through. Pauses offscreen and on hidden tabs, clamps device pixel ratio, and tears down its GL/canvas context on unmount.
