A palette is the list of colours a dither can use, ordered from paper (tone 0) to ink (tone 1). By default it's two colours, --dither-paper and --dither-ink, which dither-tokens points at your --background and --primary.
More than two colours
Components that draw with DitherShader take a palette prop with two or more colours. The tone is spread across the list, and each cell is dithered between the two neighbouring colours. With four colours, a tone of 0.5 sits halfway between the second and third:
<DitherGradient
palette={["--background", "--chart-3", "--chart-2", "--chart-1"]}
/>Each entry is a CSS variable name such as "--chart-1", or any CSS colour: "var(--chart-1)", "oklch(0.72 0.15 272)", "#7c8cff", even "transparent" to let the page show through the paper. The type is DitherPalette, a tuple of at least two DitherColor strings, so a one-colour palette fails to compile.
Order matters more than hue. Put colours in order of how much they stand out against the paper, or the ramp will jump back and forth.
Any number of hues
A field returns one tone per cell, and the palette turns that tone into a colour. So with a palette of several hues, the tone decides both how dense the dots are and which colour they take. That suits ramps, such as a glow that runs from deep blue to white, but it can't draw a red line and a blue line at the same strength, or a spectrum that runs along a line.
For that, add a colorField: a second function that returns the colour of the dots in each cell. The field still decides where the dots go, and cells without a dot are paper, palette[0]:
const COLOR_FIELD = `
vec3 colorField(vec2 uv, vec2 res, float t) {
return spectrum(uv.x * 0.75 - t * 0.03);
}
`;
<DitherShader
field={FIELD}
colorField={COLOR_FIELD}
palette={["--background", "--foreground"]}
/>;It takes the same arguments and uniforms as the field, and returns vec3 RGB or vec4 RGBA. If TypeScript says colorField doesn't exist, your copy of the engine is older than it: update the engine. Two helpers come with it:
spectrum(h)is a saturated rainbow: 0 is red, then yellow, green, cyan, blue and magenta, and red again at 1. It wraps, sohcan keep growing with time.paletteColor(x)reads the palette's ink colours, every entry after the paper, as a smooth ramp from 0 to 1. Use it to colour by position in your theme colours:return paletteColor(uv.x).rgb;withpalette={["--background", "--chart-1", "--chart-2", "--chart-3"]}.
Soft tones still dither, so a coloured glow fades out in sparse dots of its colour. To colour several shapes differently, work out which shape a cell is in with a helper. Both strings compile into one shader: define the helper in the field string, or prepend the same helpers to both (the engine keeps one copy), and call it from both functions; the demo above finds the nearest line that way and shifts each line's hue.
spectrum is fully saturated, so check light mode: yellow and cyan nearly vanish on white. The demo mixes a fifth of the theme's foreground into every hue, mix(spectrum(h), paletteColor(0.0).rgb, 0.2), which keeps it readable in both modes.
If the colour has to come from outside the shader, such as an image or a CSS gradient, put that layer under the shader and use palette={["--background", "transparent"]}: tone 0 paints the background and tone 1 shows the layer.
Following the theme
Colours are resolved in the browser, in the context of the component, so a variable you override on a wrapper applies to everything inside it. Formats the GPU can't read, such as oklch(), are converted by painting them once into a 1×1 canvas.
When the class or style on <html> changes, as it does when next-themes switches between light and dark, every dither re-reads its colours and redraws. Nothing else needs to happen.
For your own canvas work, useDitherColors does the same and returns the colours as [r, g, b, a] in [0, 1]:
const ref = useRef<HTMLDivElement>(null);
const colors = useDitherColors(ref); // [ink, paper], or null before mountPass a list of names as the second argument to read other variables, such as useDitherColors(ref, ["--chart-1", "--chart-2"]).
Static fills
DitherPattern takes a color and an optional background, and ditherMaskStyle masks whatever background colour the element already has. Both accept CSS variables, so they follow the theme without JavaScript.
Contrast
A dither reads only when ink and paper differ enough. For charts and anything that carries information, aim for the contrast you would want for body text, about 4.5:1. Quiet backgrounds can go lower, since nobody needs to read them. The defaults, your primary on your background, usually clear that bar in both modes.