# Scroll reveal image

An image that sharpens from coarse dithered blocks to the full photo as you scroll it into view.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

<ComponentPreview src="examples/scroll-reveal-image-demo.tsx" size="lg" controls="scrollRevealControls">
  <ScrollRevealImageDemo />
</ComponentPreview>

## Installation [#installation]

<InstallBlock name="scroll-reveal-image" />

## Usage [#usage]

```tsx
import { ScrollRevealImage } from "@/components/dithercn/scroll-reveal-image";

export function Chapter() {
  return (
    <ScrollRevealImage
      src="/photos/launch.jpg"
      alt="The launch pad at dawn"
      className="aspect-video w-full rounded-xl"
    />
  );
}
```

Progress follows the scroll position, not a timer. When the top of the image enters at the bottom of the viewport it is a few large blocks. It sharpens in nine steps to full dither detail, then the photo dissolves in cell by cell. It is finished when its top edge is 30% down the viewport. Scroll back and it runs in reverse.

`start` and `finish` move those two points, as a share of the viewport height from the top. Inside a scrolling panel, pass the panel's ref as `scrollRoot`, as the demo above does.

## Examples [#examples]

### In a long read [#in-a-long-read]

The image resolves as the reader reaches it. `finish={0.4}` completes it a little lower on the screen.

<ComponentPreview src="examples/scroll-reveal-image-story.tsx" size="lg">
  <ScrollRevealImageStory />
</ComponentPreview>

### Ending on dither [#ending-on-dither]

`end="dither"` stops at full dither detail and never shows the photo. A later `start` on the second image staggers the pair.

<ComponentPreview src="examples/scroll-reveal-image-gallery.tsx" size="lg">
  <ScrollRevealImageGallery />
</ComponentPreview>

## Props [#props]

Takes every look prop of [Dither image](/docs/images/dither-image) (`preset`, `color`, `palette`, `method`, `cell`, `contrast`, `brightness`, `normalize`, `sharpen`, `invert`, `fit`), plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` (required) | `string` |  | Image URL. Cross-origin images need CORS headers. |
| `alt` (required) | `string` |  | Describes the image. Pass "" when it is decorative. |
| `end` | `"photo" \| "dither"` | `"photo"` | Finish on the original photo, or on the finest dither. |
| `coarsest` | `number` | `24` | How many cells wide the first blocks are. |
| `start` | `number` | `1` | Where the image's top edge is when the reveal starts, 0 (top) to 1 (bottom of the viewport). |
| `finish` | `number` | `0.3` | Where the top edge is when the reveal finishes. |
| `scrollRoot` | `RefObject<HTMLElement \| null>` |  | The scrolling element, when it is not the page. |
| `progress` | `number` |  | Set the progress yourself, 0 to 1, instead of following the scroll. |
| `onProgressChange` | `(progress: number) => void` |  | Called when the progress changes. |
| `ratio` | `number` | `The image's ratio` | Width over height. |

## Notes [#notes]

* Under reduced motion the image is shown finished: the photo, or the fine dither with `end="dither"`.
* The root has `role="img"` and your `alt` text as its label, so screen readers hear the image once, whatever stage it is at. `data-stage` is `coarse`, `fine` or `photo`.
* Each coarseness is dithered once and cached, so scrolling back and forth costs nothing after the first pass. Scroll handling only runs while the image is near the viewport, at most once per frame.