Dither Gradient
FeaturedA two-tone field resolved by an 8x8 Bayer matrix — the 1-bit look, with the dither grid pinned to layout pixels so it survives a retina screen.
- lines of CSS
- 24lines of CSS
- dependencies
- 0dependencies
- browser, for years
- Everybrowser, for years
- no motion at all
- Noneno motion at all
'use client'
/**
* Dither Gradient — a two-tone gradient, resolved by an 8×8 ordered matrix.
*
* A gradient is the one thing CSS does perfectly, so this is deliberately
* the opposite of one: the field is quantised to two or three tones and the
* in-between values are carried by a Bayer threshold pattern instead of by
* colour. What you get is the 1-bit look — Playdate, old Mac, Obra Dinn —
* which cannot be approximated by `background-image` at any size, because
* the pattern has to be evaluated per pixel against a moving field.
*
* The dither grid is expressed against `u_resolution`, not `gl_FragCoord`.
* Threshold patterns are usually written in device pixels, and on a 2x
* screen that halves the apparent cell size — a dither that disappears on
* the machines designers own is not a dither.
*/
import * as React from 'react'
import { ShaderSurface } from './shader-surface'
import type { GLProgram } from './shader-runtime'
export const DITHER_GRADIENT_PROGRAM: GLProgram = {
id: 'dither-gradient',
kind: 'webgl',
speed: 1,
light: { c0: '#334155', c1: '#0f172a', c2: '#64748b', bg: '#f1f5f9' },
dark: { c0: '#a3e635', c1: '#ecfccb', c2: '#365314', bg: '#0a0f07' },
fragment: `
vec4 render(vec2 uv) {
// 140 dither cells across the short axis, whatever the device pixel
// ratio is. Aspect-corrected so the cells stay square.
vec2 cell = floor(uv * vec2(u_resolution.x / u_resolution.y, 1.0) * 140.0);
vec2 p = aspect(uv);
float t = u_time * 0.09;
/*
* The field being dithered: a vertical ramp with slow noise on top, so
* the boundary between tones creeps rather than sitting still. The ramp
* has to dominate — weighted evenly with the noise, as it was, the tone
* boundaries wander far enough that the result reads as static rather
* than as a dithered gradient.
*/
float field = 0.95 * (1.0 - uv.y) + 0.38 * fbm(p * 1.5 + vec2(t, t * 0.6));
float threshold = bayer8(cell);
float lit = step(threshold, field);
float hot = step(threshold + 0.34, field);
vec3 col = mix(u_bg, u_c2, lit);
col = mix(col, u_c0, hot);
col = mix(col, u_c1, step(threshold + 0.62, field));
return vec4(col, 1.0);
}
`,
}
export function DitherGradient({ className }: { className?: string }) {
return <ShaderSurface program={DITHER_GRADIENT_PROGRAM} className={className} />
}
What's included
- The markup and the stylesheet, in whichever framework you pick
- No runtime dependencies
Works with
- React
- Vue
- Svelte
- Tailwind
- styled-components
- Plain CSS
npx hoverlab add dither-gradient
Or over MCP, from your editor's agent — no account needed.
License
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Motion safety
Not profiled. Canvas / WebGL effect: its deliverable is a shader, and the motion profile only reads CSS.
32 more effects in Filters & Blend Modes
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse Filters & Blend ModesShipping one commercially
Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
- A commercial licence for everything in the catalog
- Unlimited bundle exports, in Vue, Svelte and Tailwind
- One payment — no subscription, nothing to renew