Dither Background
Use When
- A dark interface needs an atmospheric monochrome background layer.
- The visual should show enlarged square pixels and visible ordered dithering.
- The design calls for organic waves, cloud-like masses, or procedural depth without colorful gradients.
- The background should support framed UI, hero content, or data overlays.
Visual Target
- Near-black base with charcoal midtones, soft gray buildup, and occasional white highlights.
- Clearly visible square pixel cells, not tiny film grain.
- 4x4 Bayer-style dither pattern or equivalent ordered thresholding.
- Broad organic waves or cloud-like masses, not random TV noise.
- Vignetted edges so the brighter mass sits centrally or off-axis.
HTML And CSS
<canvas class="dither-background" data-dither-background></canvas>
.dither-background {
position: fixed;
inset: 0;
z-index: 0;
width: 100vw;
height: 100vh;
background: #030303;
pointer-events: none;
}
.page-content {
position: relative;
z-index: 1;
}
Canvas Recipe
Use a real canvas when motion or procedural depth is needed.
const BAYER_4X4 = [
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5,
].map((value) => (value + 0.5) / 16);
function smoothstep(edge0, edge1, value) {
const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
function noise2(x, y) {
const value = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123;
return value - Math.floor(value);
}
function valueNoise(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
const a = noise2(ix, iy);
const b = noise2(ix + 1, iy);
const c = noise2(ix, iy + 1);
const d = noise2(ix + 1, iy + 1);
return (
a * (1 - ux) * (1 - uy) +
b * ux * (1 - uy) +
c * (1 - ux) * uy +
d * ux * uy
);
}
function fbm(x, y) {
let value = 0;
let amplitude = 0.5;
let frequency = 1;
for (let octave = 0; octave < 4; octave++) {
value += valueNoise(x * frequency, y * frequency) * amplitude;
frequency *= 2.02;
amplitude *= 0.5;
}
return value;
}
function initDitherBackground(canvas, options = {}) {
if (!canvas) return () => {};
const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) return () => {};
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cell = options.cellSize || 7;
const maxDpr = options.maxDpr || 1.5;
let width = 1;
let height = 1;
let cols = 1;
let rows = 1;
let rafId = 0;
const palette = options.palette || [
[3, 3, 3],
[16, 16, 17],
[34, 35, 37],
[74, 75, 78],
[168, 169, 171],
[236, 236, 232],
];
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
width = Math.max(1, window.innerWidth);
height = Math.max(1, window.innerHeight);
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cols = Math.ceil(width / cell);
rows = Math.ceil(height / cell);
}
function sampleField(x, y, time) {
const nx = (x / cols - 0.5) * 2;
const ny = (y / rows - 0.5) * 2;
const distance = Math.sqrt(nx * nx * 0.84 + ny * ny * 1.28);
const vignette = 1 - smoothstep(0.18, 1.15, distance);
const drift = reduceMotion ? 0 : time * 0.018;
const wave =
Math.sin(nx * 2.8 + ny * 1.2 + drift) * 0.18 +
Math.sin(nx * -1.4 + ny * 3.8 - drift * 0.8) * 0.14;
const cloud = fbm(nx * 1.35 + drift * 0.16, ny * 1.35 - drift * 0.08);
const ridge = smoothstep(0.48, 0.92, cloud + wave);
const offAxisMass = smoothstep(0.98, 0.18, Math.hypot(nx + 0.22, ny - 0.08));
return Math.max(0, Math.min(1, ridge * vignette * 0.92 + offAxisMass * 0.18));
}
function render(time = 0) {
const seconds = time * 0.001;
ctx.fillStyle = "rgb(3,3,3)";
ctx.fillRect(0, 0, width, height);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const threshold = BAYER_4X4[(y % 4) * 4 + (x % 4)];
const brightness = sampleField(x, y, seconds);
const stepped = Math.floor(Math.max(0, Math.min(0.999, brightness + threshold * 0.18)) * palette.length);
const color = palette[Math.min(palette.length - 1, stepped)];
ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
ctx.fillRect(x * cell, y * cell, cell, cell);
}
}
if (!reduceMotion) rafId = requestAnimationFrame(render);
}
function handleResize() {
cancelAnimationFrame(rafId);
resize();
render();
}
resize();
render();
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", handleResize);
};
}
const cleanupDither = initDitherBackground(
document.querySelector("[data-dither-background]"),
{
cellSize: 7,
maxDpr: 1.5,
}
);
Tuning Knobs
- Cell size:
5px-10px; larger cells make the Bayer matrix more legible.
- Palette: near-black, charcoal, soft gray, rare white highlights only.
- Shape: tune
wave, cloud, ridge, and offAxisMass to create broad masses.
- Vignette: increase edge falloff when foreground readability needs more quiet.
- Motion: keep drift slow; use low time multipliers and avoid flicker.
- Performance: increase
cellSize or cap maxDpr before simplifying the field.
Composition Notes
- Put the canvas behind the interface with
pointer-events: none.
- Use it as atmosphere behind framed UI, hero copy, or data overlays.
- Keep foreground contrast controlled and typography clean.
- Let one main bright mass define the composition; avoid even full-screen brightness.
Avoid
- Rainbow gradients, colorful noise, or soft blurry blobs without dither structure.
- Tiny grain or static-like speckle where the square matrix disappears.
- Bright full-screen white noise that competes with foreground type.
- Random per-frame noise that flickers instead of drifting.
- Covering the entire viewport with equally bright cells.
Quick Checks
- The square Bayer pattern is visible from normal viewing distance.
- The field forms broad organic waves or cloud masses.
- The palette stays monochrome and restrained.
- Edges recede into near-black.
- Foreground UI remains readable without heavy overlays.
1---2name: dither-background3description: Create a dark monochrome procedural background with enlarged square pixels and visible Bayer-style ordered dithering. Use when a page needs an atmospheric near-black dither field, broad organic waves or cloud masses, and restrained gray-white highlights behind framed UI, hero content, or data overlays.4---5
6# Dither Background
7
8## Use When
9- A dark interface needs an atmospheric monochrome background layer.
10- The visual should show enlarged square pixels and visible ordered dithering.
11- The design calls for organic waves, cloud-like masses, or procedural depth without colorful gradients.
12- The background should support framed UI, hero content, or data overlays.
13
14## Visual Target
15- Near-black base with charcoal midtones, soft gray buildup, and occasional white highlights.
16- Clearly visible square pixel cells, not tiny film grain.
17- 4x4 Bayer-style dither pattern or equivalent ordered thresholding.
18- Broad organic waves or cloud-like masses, not random TV noise.
19- Vignetted edges so the brighter mass sits centrally or off-axis.
20
21## HTML And CSS
22
23```html
24<canvas class="dither-background" data-dither-background></canvas>
25```
26
27```css
28.dither-background {
29 position: fixed;
30 inset: 0;
31 z-index: 0;
32 width: 100vw;
33 height: 100vh;
34 background: #030303;
35 pointer-events: none;
36}
37
38.page-content {
39 position: relative;
40 z-index: 1;
41}
42```
43
44## Canvas Recipe
45Use a real canvas when motion or procedural depth is needed.
46
47```js
48const BAYER_4X4 = [
49 0, 8, 2, 10,
50 12, 4, 14, 6,
51 3, 11, 1, 9,
52 15, 7, 13, 5,
53].map((value) => (value + 0.5) / 16);
54
55function smoothstep(edge0, edge1, value) {
56 const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
57 return t * t * (3 - 2 * t);
58}
59
60function noise2(x, y) {
61 const value = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123;
62 return value - Math.floor(value);
63}
64
65function valueNoise(x, y) {
66 const ix = Math.floor(x);
67 const iy = Math.floor(y);
68 const fx = x - ix;
69 const fy = y - iy;
70 const ux = fx * fx * (3 - 2 * fx);
71 const uy = fy * fy * (3 - 2 * fy);
72
73 const a = noise2(ix, iy);
74 const b = noise2(ix + 1, iy);
75 const c = noise2(ix, iy + 1);
76 const d = noise2(ix + 1, iy + 1);
77 return (
78 a * (1 - ux) * (1 - uy) +
79 b * ux * (1 - uy) +
80 c * (1 - ux) * uy +
81 d * ux * uy
82 );
83}
84
85function fbm(x, y) {
86 let value = 0;
87 let amplitude = 0.5;
88 let frequency = 1;
89
90 for (let octave = 0; octave < 4; octave++) {
91 value += valueNoise(x * frequency, y * frequency) * amplitude;
92 frequency *= 2.02;
93 amplitude *= 0.5;
94 }
95
96 return value;
97}
98
99function initDitherBackground(canvas, options = {}) {
100 if (!canvas) return () => {};
101
102 const ctx = canvas.getContext("2d", { alpha: false });
103 if (!ctx) return () => {};
104
105 const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
106 const cell = options.cellSize || 7;
107 const maxDpr = options.maxDpr || 1.5;
108 let width = 1;
109 let height = 1;
110 let cols = 1;
111 let rows = 1;
112 let rafId = 0;
113
114 const palette = options.palette || [
115 [3, 3, 3],
116 [16, 16, 17],
117 [34, 35, 37],
118 [74, 75, 78],
119 [168, 169, 171],
120 [236, 236, 232],
121 ];
122
123 function resize() {
124 const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
125 width = Math.max(1, window.innerWidth);
126 height = Math.max(1, window.innerHeight);
127 canvas.width = Math.floor(width * dpr);
128 canvas.height = Math.floor(height * dpr);
129 canvas.style.width = `${width}px`;
130 canvas.style.height = `${height}px`;
131 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
132 cols = Math.ceil(width / cell);
133 rows = Math.ceil(height / cell);
134 }
135
136 function sampleField(x, y, time) {
137 const nx = (x / cols - 0.5) * 2;
138 const ny = (y / rows - 0.5) * 2;
139 const distance = Math.sqrt(nx * nx * 0.84 + ny * ny * 1.28);
140 const vignette = 1 - smoothstep(0.18, 1.15, distance);
141 const drift = reduceMotion ? 0 : time * 0.018;
142
143 const wave =
144 Math.sin(nx * 2.8 + ny * 1.2 + drift) * 0.18 +
145 Math.sin(nx * -1.4 + ny * 3.8 - drift * 0.8) * 0.14;
146 const cloud = fbm(nx * 1.35 + drift * 0.16, ny * 1.35 - drift * 0.08);
147 const ridge = smoothstep(0.48, 0.92, cloud + wave);
148 const offAxisMass = smoothstep(0.98, 0.18, Math.hypot(nx + 0.22, ny - 0.08));
149
150 return Math.max(0, Math.min(1, ridge * vignette * 0.92 + offAxisMass * 0.18));
151 }
152
153 function render(time = 0) {
154 const seconds = time * 0.001;
155 ctx.fillStyle = "rgb(3,3,3)";
156 ctx.fillRect(0, 0, width, height);
157
158 for (let y = 0; y < rows; y++) {
159 for (let x = 0; x < cols; x++) {
160 const threshold = BAYER_4X4[(y % 4) * 4 + (x % 4)];
161 const brightness = sampleField(x, y, seconds);
162 const stepped = Math.floor(Math.max(0, Math.min(0.999, brightness + threshold * 0.18)) * palette.length);
163 const color = palette[Math.min(palette.length - 1, stepped)];
164 ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
165 ctx.fillRect(x * cell, y * cell, cell, cell);
166 }
167 }
168
169 if (!reduceMotion) rafId = requestAnimationFrame(render);
170 }
171
172 function handleResize() {
173 cancelAnimationFrame(rafId);
174 resize();
175 render();
176 }
177
178 resize();
179 render();
180 window.addEventListener("resize", handleResize);
181
182 return () => {
183 cancelAnimationFrame(rafId);
184 window.removeEventListener("resize", handleResize);
185 };
186}
187
188const cleanupDither = initDitherBackground(
189 document.querySelector("[data-dither-background]"),
190 {
191 cellSize: 7,
192 maxDpr: 1.5,
193 }
194);
195```
196
197## Tuning Knobs
198- Cell size: `5px-10px`; larger cells make the Bayer matrix more legible.
199- Palette: near-black, charcoal, soft gray, rare white highlights only.
200- Shape: tune `wave`, `cloud`, `ridge`, and `offAxisMass` to create broad masses.
201- Vignette: increase edge falloff when foreground readability needs more quiet.
202- Motion: keep drift slow; use low time multipliers and avoid flicker.
203- Performance: increase `cellSize` or cap `maxDpr` before simplifying the field.
204
205## Composition Notes
206- Put the canvas behind the interface with `pointer-events: none`.
207- Use it as atmosphere behind framed UI, hero copy, or data overlays.
208- Keep foreground contrast controlled and typography clean.
209- Let one main bright mass define the composition; avoid even full-screen brightness.
210
211## Avoid
212- Rainbow gradients, colorful noise, or soft blurry blobs without dither structure.
213- Tiny grain or static-like speckle where the square matrix disappears.
214- Bright full-screen white noise that competes with foreground type.
215- Random per-frame noise that flickers instead of drifting.
216- Covering the entire viewport with equally bright cells.
217
218## Quick Checks
219- The square Bayer pattern is visible from normal viewing distance.
220- The field forms broad organic waves or cloud masses.
221- The palette stays monochrome and restrained.
222- Edges recede into near-black.
223- Foreground UI remains readable without heavy overlays.