Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/dither-image.jsonUsage
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
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. |
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.
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
A fixed palette ignores the theme, so the newsprint card looks the same on a black page and a white one.
Props
| Prop | Type | Default |
|---|---|---|
| src* Image URL. Images from another origin need CORS headers, because the pixels are read from a canvas. | Type: string | Default – |
| alt* Describes the image for screen readers. Pass "" when it is decorative. | Type: string | Default – |
| preset A starting look: method, cell, contrast and palette. | Type: "ink" | "duotone" | "newsprint" | "gameboy" | "riso" | Default "duotone" |
| color The ink for the ink and duotone presets. A palette name or any CSS colour. | Type: DitherColor | Default "ink" |
| palette Two to four colours that replace the preset palette. Sorted dark to light. Use "transparent" to let the background show through. | Type: [DitherColor, DitherColor, ...DitherColor[]] | Default From the preset |
| method The dither algorithm. | Type: "bayer4" | "bayer8" | "blue-noise" | "floyd-steinberg" | "atkinson" | "sierra-lite" | Default From the preset |
| cell Size of one cell in CSS pixels, rounded to whole device pixels. | Type: number | Default From the preset (2 or 3) |
| contrast 1 leaves contrast alone. Higher values push tones apart. | Type: number | Default From the preset |
| brightness Added to every tone, from -1 to 1. | Type: number | Default 0 |
| normalize Stretch the image's darkest and lightest tones to the ends of the palette. | Type: boolean | Default true |
| sharpen Edge sharpening before dithering, 0 to 1. | Type: number | Default 0.3 |
| invert Swap light and dark. | Type: boolean | Default false |
| fit How the image fills its box, as in object-fit. | Type: "cover" | "contain" | Default "cover" |
| ratio Width over height. Reserves space before the image loads. A height in className wins. | Type: number | Default The image's ratio |
| resolve Resolve from coarse blocks to full detail the first time the image appears. | Type: boolean | Default true |
Notes
- The root has
role="img"and youralttext as its label, andaria-busywhile it loads. Withalt=""it is hidden from assistive technology. data-stateon the root isidle,loading,resolving,readyorerror.- 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.