# 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).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/dither-image.json](https://dithercn.dev/r/dither-image.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/dither-image.json
```

## 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.                    |

```tsx title="examples/dither-image-presets.tsx"
"use client";

import { DitherImage } from "@/components/dithercn/dither-image";
import type { DitherImageProps } from "@/components/dithercn/dither-image";

const LOOKS: readonly {
  label: string;
  look: Pick<DitherImageProps, "preset" | "color" | "palette">;
}[] = [
  { label: "ink", look: { color: "orange", preset: "ink" } },
  { label: "duotone", look: { color: "orange", preset: "duotone" } },
  { label: "newsprint", look: { preset: "newsprint" } },
  { label: "gameboy", look: { preset: "gameboy" } },
  { label: "riso", look: { preset: "riso" } },
  {
    label: "palette: paper, green, pink",
    look: { palette: ["--dither-paper", "green", "pink"] },
  },
];

export default function DitherImagePresets() {
  return (
    <div className="grid w-full grid-cols-2 gap-x-3 gap-y-5 sm:grid-cols-3">
      {LOOKS.map(({ label, look }) => (
        <figure key={label} className="flex flex-col gap-2">
          <DitherImage
            {...look}
            src="/examples/tetons.jpg"
            alt=""
            ratio={5 / 4}
            className="w-full rounded-lg"
          />
          <figcaption className="text-muted-foreground truncate font-mono text-xs">
            {label}
          </figcaption>
        </figure>
      ))}
    </div>
  );
}
```

### 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.

```tsx title="examples/dither-image-duotone.tsx"
import { DitherImage } from "@/components/dithercn/dither-image";
import type { DitherColorName } from "@/lib/dither";

const MISSIONS: readonly {
  name: string;
  year: string;
  src: string;
  alt: string;
  color: DitherColorName;
}[] = [
  {
    alt: "Earth rising over the Moon's horizon",
    color: "blue",
    name: "Apollo 8",
    src: "/examples/earthrise.jpg",
    year: "1968",
  },
  {
    alt: "Buzz Aldrin standing on the Moon",
    color: "orange",
    name: "Apollo 11",
    src: "/examples/aldrin.jpg",
    year: "1969",
  },
  {
    alt: "The Pillars of Creation in the Eagle Nebula",
    color: "pink",
    name: "Hubble",
    src: "/examples/pillars.jpg",
    year: "2014",
  },
];

export default function DitherImageDuotone() {
  return (
    <div className="grid w-full gap-4 sm:grid-cols-3">
      {MISSIONS.map((mission) => (
        <article
          key={mission.name}
          className="bg-background flex flex-col overflow-hidden rounded-xl border"
        >
          <DitherImage
            src={mission.src}
            alt={mission.alt}
            preset="duotone"
            color={mission.color}
            ratio={1}
            className="w-full [--dither-paper:var(--background)]"
          />
          <div className="flex items-baseline justify-between px-4 py-3">
            <span className="text-sm font-medium">{mission.name}</span>
            <span className="text-muted-foreground font-mono text-xs">
              {mission.year}
            </span>
          </div>
        </article>
      ))}
    </div>
  );
}
```

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.

```tsx title="examples/dither-image-newsprint.tsx"
import { DitherImage } from "@/components/dithercn/dither-image";

export default function DitherImageNewsprint() {
  return (
    <article className="flex w-full max-w-2xl flex-col gap-5 rounded-2xl bg-[oklch(0.93_0.022_88)] p-5 text-[oklch(0.22_0.012_70)] sm:flex-row sm:p-6">
      <DitherImage
        src="/examples/earthrise.jpg"
        alt="Earth rising over the Moon's horizon, photographed from Apollo 8"
        preset="newsprint"
        ratio={1}
        className="w-full shrink-0 sm:w-56"
      />
      <div className="flex flex-col gap-3">
        <span className="font-mono text-[11px] tracking-[0.14em] uppercase">
          Late edition · 25 December 1968
        </span>
        <h3 className="font-serif text-2xl leading-tight font-semibold text-balance">
          Three men orbit the Moon and see the Earth rise
        </h3>
        <p className="text-sm leading-relaxed opacity-80">
          The newsprint preset uses an ordered 8 by 8 screen and its own warm
          paper, so the page reads as print in both themes.
        </p>
      </div>
    </article>
  );
}
```

## 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.