# Dither image

Any photo dithered to a preset or your own palette, with crisp pixels and a coarse-to-fine reveal as it loads.

> 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/dither-image-demo.tsx" size="lg" controls="ditherImageControls">
  <DitherImageDemo />
</ComponentPreview>

## Installation [#installation]

<InstallBlock name="dither-image" />

## Usage [#usage]

```tsx
import { DitherImage } from "@/components/dithercn/dither-image";

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

The default `duotone` preset suits most photos. Pick another `preset`, then override any part of it. The image fills its box like `object-fit: cover`; give the box a width and a height or aspect ratio, or set `ratio` so the space is reserved before the image loads.

Every cell is a whole number of device pixels and the canvas is scaled up with `image-rendering: pixelated`, so edges stay sharp at any zoom. Before dithering, the tones are stretched to fill the palette and edges get a light sharpen, which keeps faces and horizons readable at a 2px cell. Turn these off with `normalize={false}` and `sharpen={0}`.

While the file loads the box shows a faint dotted field. When it arrives the image resolves from 16× blocks to full detail in about a third of a second.

## Examples [#examples]

### Presets [#presets]

| Preset      | Look                                                                    |
| ----------- | ----------------------------------------------------------------------- |
| `ink`       | Atkinson diffusion, one ink on the page colour. Follows the theme.      |
| `duotone`   | Blue noise over the page colour, a tint and the ink. Follows the theme. |
| `newsprint` | An ordered 8×8 screen, near black on warm paper, in both themes.        |
| `gameboy`   | Four greens on a 3px Bayer grid.                                        |
| `riso`      | Navy, blue and pink on cream, like a two-drum print.                    |

<ComponentPreview src="examples/dither-image-presets.tsx" size="lg">
  <DitherImagePresets />
</ComponentPreview>

### Palettes from colour names [#palettes-from-colour-names]

`color` takes any palette name: `blue`, `purple`, `green`, `pink`, `orange`, `red`, `grey` or `ink`. Each is tuned for light and dark. Here the duotone preset gives each mission its own hue.

<ComponentPreview src="examples/dither-image-duotone.tsx" size="lg">
  <DitherImageDuotone />
</ComponentPreview>

For full control, pass `palette` with two to four colours. Names, CSS variables and CSS colours all work: `palette={["--dither-paper", "purple", "oklch(0.9 0.05 300)"]}`.

### Newsprint article [#newsprint-article]

A fixed palette ignores the theme, so the newsprint card looks the same on a black page and a white one.

<ComponentPreview src="examples/dither-image-newsprint.tsx" size="lg">
  <DitherImageNewsprint />
</ComponentPreview>

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` (required) | `string` |  | Image URL. Images from another origin need CORS headers, because the pixels are read from a canvas. |
| `alt` (required) | `string` |  | Describes the image for screen readers. Pass "" when it is decorative. |
| `preset` | `"ink" \| "duotone" \| "newsprint" \| "gameboy" \| "riso"` | `"duotone"` | A starting look: method, cell, contrast and palette. |
| `color` | `DitherColor` | `"ink"` | The ink for the ink and duotone presets. A palette name or any CSS colour. |
| `palette` | `[DitherColor, DitherColor, ...DitherColor[]]` | `From the preset` | Two to four colours that replace the preset palette. Sorted dark to light. Use "transparent" to let the background show through. |
| `method` | `"bayer4" \| "bayer8" \| "blue-noise" \| "floyd-steinberg" \| "atkinson" \| "sierra-lite"` | `From the preset` | The dither algorithm. |
| `cell` | `number` | `From the preset (2 or 3)` | Size of one cell in CSS pixels, rounded to whole device pixels. |
| `contrast` | `number` | `From the preset` | 1 leaves contrast alone. Higher values push tones apart. |
| `brightness` | `number` | `0` | Added to every tone, from -1 to 1. |
| `normalize` | `boolean` | `true` | Stretch the image's darkest and lightest tones to the ends of the palette. |
| `sharpen` | `number` | `0.3` | Edge sharpening before dithering, 0 to 1. |
| `invert` | `boolean` | `false` | Swap light and dark. |
| `fit` | `"cover" \| "contain"` | `"cover"` | How the image fills its box, as in object-fit. |
| `ratio` | `number` | `The image's ratio` | Width over height. Reserves space before the image loads. A height in className wins. |
| `resolve` | `boolean` | `true` | Resolve from coarse blocks to full detail the first time the image appears. |

## Notes [#notes]

* The root has `role="img"` and your `alt` text as its label, and `aria-busy` while it loads. With `alt=""` it is hidden from assistive technology.
* `data-state` on the root is `idle`, `loading`, `resolving`, `ready` or `error`.
* The image loads when it comes within about 300px of the viewport. It redraws when the box resizes, the page zooms or the theme changes, without resolving again.
* Under reduced motion the image appears at full detail with no resolve.
* Dithering runs on the main thread at one pixel per cell. A 1200 by 800 pixel box at a 2px cell is 240,000 pixels, a few milliseconds of work. Coarse frames are much cheaper.