Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/moire.jsonUsage
import { Moire } from "@/components/dithercn/moire";
export function Hero() {
return (
<section className="relative isolate min-h-[480px]">
<Moire className="absolute inset-0 -z-10" />
<h1>Your headline</h1>
</section>
);
}Two gratings of fine lines sit on top of each other. Each line is a whole number of dither cells wide, every pitch cells, so the lines stay crisp at any size. A line of one grating is drawn in the second palette colour, and a cell where lines of both gratings cross is drawn in the last. Where the gratings line up, their lines overlap into crisp ink stripes on open paper. Where they fall half a pitch apart, they fill each other's gaps with a flat tint. Between the two, the stripes thicken and soften, and the eye reads the change as broad bands many times larger than the lines themselves.
The bands move far more than the lines do. The gratings slide apart by a fraction of a cell per second, and the bands sweep across the box at dozens of pixels per second. At the same time the angle between the gratings breathes, so the bands swell, narrow and swing. The second grating is never quite flat: warp bends it by a cell or two in a few broad swells that change shape slowly. That bend is too small to see in the lines, but it curves the bands by most of their width. There are three variants:
"lines": two sets of straight lines a few degrees apart. The bands run across the lines."rings": two sets of concentric rings whose centres drift apart and together. The bands fan out from the middle as a slowly turning starburst."mesh": two square grids, like a pair of window screens. The bands form a large lattice of their own.
bands sets how many bands cross the box, not how far the lines are turned, so a phone shows the same composition as a desktop.
Examples
Rings in a palette
colors gives each band the next colour from a list in turn. The lines that cross take the band's colour and the rest a tint of it, and the colours change halfway between two bands, inside the flat tint, so no colour edge cuts across the crisp stripes. With "rings", the bands fan out from the middle, so the colours do too. Ring colours run into each other across most of each band, and they fade towards grey near the middle, where every band meets, so the centre shows the rings' own interference rather than slices of colour.
Behind a headline
mask="center" clears an oval in the middle of the box, and the lines dissolve into dots towards it. Two big bands and two soft tints of the ink keep the pattern quiet behind the text.
Props
| Prop | Type | Default |
|---|---|---|
| variant Two sets of straight lines, two sets of concentric rings, or two square grids. | Type: "lines" | "rings" | "mesh" | Default "lines" |
| pitch Spacing of each grating's lines, in whole dither cells, from 3 to 12 (4 to 12 for mesh). Each line is half a pitch wide. | Type: number | Default 4 |
| bands Roughly how many interference bands cross the box, from 1 to 8. Fewer bands are bigger. | Type: number | Default 3 |
| angle Direction of the lines in degrees, as in CSS: 0 runs up, 90 across. For rings, the direction from one ring centre to the other. | Type: number | Default 30 |
| center The point the gratings turn around, or the midpoint between the ring centres, as fractions of the box from the top left. | Type: [x: number, y: number] | Default [0.5, 0.5] |
| drift How fast the gratings slide against each other, so the bands sweep across the box. 0 stops the slide; the bands still turn and breathe. | Type: number | Default 1 |
| warp How much the second grating bends, so the bands curve and twist instead of running straight, from 0 (flat) to 2. | Type: number | Default 0.6 |
| mask Fades it to paper where your text sits. | Type: "none" | "center" | "top" | "bottom" | "edges" | Default "none" |
| colors Colour each band from this list in turn. Palette names such as "blue", CSS variable names, or any CSS colour. The palette then only sets the paper. | Type: [DitherColor, ...DitherColor[]] | Default – |
| palette Colours from paper to ink. Lines of one grating take the colour halfway along, cells where both cross the last. With two colours, lines that don't cross dither to half. | Type: [DitherColor, DitherColor, ...DitherColor[]] | Default Paper, a tint of --dither-ink, then --dither-ink |
| matrix The threshold pattern, seen where a mask fades the lines. See Dither engine. | Type: "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "bayer8" |
| cell Size of one dither cell in CSS pixels, and so of the finest line. | Type: number | Default --dither-cell |
| speed How fast everything moves. | Type: number | Default 1 |
| animate false stops the motion: a still frame if it starts off, the current frame if it was moving. | Type: boolean | Default true |
| className Size and position. The gratings fill this box. | Type: string | Default – |
Notes
- It is decorative and hidden from assistive technology. Put real text on top of it, not inside it.
- With reduced motion turned on, it draws one still frame and stops.
- It pauses when scrolled out of view or when the tab is hidden, and releases its WebGL context when far offscreen. Without WebGL it draws the same two gratings as CSS repeating gradients, in the palette's middle colour, with the last colour where their lines cross, so the moiré still appears. An SVG displacement filter bends the second grating by
warp, so the bands curve, though not in exactly the same shape as the shader's. Withcolors, the colour stripes stay straight while the bands bend, and the ring colours blend smoothly instead of as dots. - Each line is drawn in whole cells and every cell is either on a line or not, so the fine pattern never blurs into grey and never flickers. No line moves more than a cell every few seconds; only the bands move quickly.
- It costs a few dot products per cell, at 30 fps and one pixel per dither cell. See Performance.