Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Free

Dither Gradient Shader for React

Soft orbs on warm paper, every pixel snapped to an ordered 1-bit dither in two to five inks, like an old Mac or a Playdate screen. For React with Base UI, when a hero, a pricing card or an indie product page wants retro pixel texture.

Framework

Dither Gradient Shader draws a gradient the way a 1-bit screen would. Three soft orbs, a warm vermilion one, a deep ink-violet one and a glow of bare paper, drift and cross on warm paper along slow Lissajous paths, bent by a little noise. Every pixel is then snapped to a chunky 3-pixel grid and to a handful of flat inks by an ordered 8×8 Bayer dither, so the blends become the cross-hatched textures of a classic Mac. The orbs fade to bare paper at their edges and ease off in the middle, which stays calm for text. A WGSL fragment shader on WebGPU computes it each frame, for about 0.15 ms of GPU time.

Use it behind an indie product's hero, a pricing card, a game or a zine. For round print dots sized by tone instead, use the Halftone background; for four-color print with rosettes, CMYK Halftone Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a CSS gradient in the same inks in browsers without WebGPU.

Preview width

Installation

Sign in to get your install command
Free with an account. It carries your token, so the CLI installs everything in one go.

Usage

import {
  DitherGradientShader,
} from "@/components/ui/dither-gradient-shader";
<DitherGradientShader className="flex min-h-[32rem] items-center justify-center">
  <h1 className="font-mono text-5xl font-semibold text-[#1d1a2f]">Made by hand</h1>
</DitherGradientShader>

Dither Gradient Shader is a container: your content sits on top of the dither, in normal flow. It fills its own box, so give it a height or content. For a backdrop behind the whole page, render it empty with className="fixed inset-0 -z-10". The paper is light in both color schemes, so use dark text, ideally the ink color: it reads at 14:1 on the paper and 5.5:1 on the vermilion, but not on the ink itself. Keep copy in the calm middle, or put it on a paper panel such as bg-[#efe9dc]/80, which keeps ink text above 9:1 everywhere.

Demos

One bit, in a card

levels={2} drops the vermilion and leaves two inks, paper and ink: a true 1-bit screen, where every shade is a pattern of ink pixels.

Preview width

Fine

pixelSize={1} makes each dither pixel one CSS pixel, a crisp, fine texture on a retina screen, and levels={5} adds two mixed inks, between paper and vermilion and between vermilion and ink, for smoother steps.

Preview width

Performance

About 0.15 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. pixelSize and levels don't change the cost. Keep the default quality: the dither is made of hard pixel edges, and half resolution would blur them. The animation stops when the shader scrolls out of view, and the browser pauses it in a hidden tab. Every shader on a page shares one GPU device and one frame loop, and vgpu (about 50 KB gzipped) loads only when the first one mounts.

Browser support

The shader needs WebGPU: Chrome and Edge, Safari 26 and later, and Firefox on Windows and macOS. Elsewhere, and until the first frame, the box shows soft CSS orbs of the same inks under a faint checker, so the layout never jumps. data-state on the root reads unsupported there, if you want to style around it. See Shader Canvas.

Accessibility

The dither is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame and stops. Nothing flashes: the orbs take half a minute or more to cross the screen, and the dither pattern is fixed to the screen, so only the pixels at an orb's moving edge change, a few at a time; measured over 6 seconds with WCAG's area test, and even in 32-pixel patches, it never flashes. Use dark text, as in Usage.

FAQ

What is ordered dithering?

A way to show shades with only a few flat colors. Each pixel compares the gradient's value against its own entry in an 8×8 grid of thresholds, the Bayer matrix, and takes the ink above or below. The grid repeats, so a mid shade becomes a regular cross-hatch. The thresholds are computed from each pixel's coordinates, bit by bit, with no texture.

What does pattern change?

pattern="bayer", the default, gives the regular cross-hatches of an old Mac. pattern="noise" swaps in interleaved gradient noise, a fine, even scatter with no visible grid.

Can I change the colors?

Yes: paperColor, accentColor and inkColor take any CSS color, var(--primary) included, and follow a theme switch. The ramp runs paper, then accent, then ink, so keep it in that order of lightness for the orbs to read as shading. With levels={2} only paper and ink are used.

How do the levels work?

levels is the number of inks, paper included, from 2 to 5. Three gives paper, vermilion and ink; four and five add mixes between them, for gentler steps. pixelSize sets how big each dither pixel is, from 1 to 8 CSS pixels.

Can it follow the pointer?

Yes: with interactive, a smaller orb of paper light follows the pointer and clears the dither around it. It fades in as the pointer comes over the box and out when it leaves, so at rest the gradient is the same as without interactive.

Will it slow my page down?

It's one of the lightest shaders here: about 0.15 ms of GPU time a frame at 1280×720 on a laptop, and the CPU only sets a few numbers each frame. It stops when it's off screen.

API Reference

DitherGradientShader

data-slot="dither-gradient-shader"

PropType
accentColor?string

The middle ink, between paper and ink: the warm orb. Any CSS colour. @default "#ff5b3a".

Default
"#ff5b3a"
fallback?string

What shows until the first frame and in browsers without WebGPU: a CSS background value (gradients, sizes, a colour) that looks like the shader. It can use the colour props' --shader-<field> variables.

inkColor?string

The darkest ink: the deep orb. Any CSS colour. @default "#1d1a2f".

Default
"#1d1a2f"
intensity?number

How much of the paper the orbs cover, from 0 (bare paper) to 2. @default 1.

Default
1
interactive?boolean

Whether u.pointer follows the pointer. @default false.

Default
false
levels?number

How many inks the dither uses, paper included, from 2 (one bit: paper and ink) to 5. @default 3.

Default
3
paperColor?string

The paper. Any CSS colour. @default "#efe9dc".

Default
"#efe9dc"
pattern?keyof typeof PATTERNS

The dither: an ordered 8×8 Bayer grid of cross-hatches, or a fine, even noise. @default "bayer".

Default
"bayer"
paused?boolean

Holds the current frame. @default false.

pixelSize?number

The size of a dither pixel, from 1 to 8 CSS pixels. @default 3.

Default
3
quality?number

Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.

speed?number

Animation speed: 1 is normal, 0 holds the frame. @default 1.

time?number

A fixed moment, in seconds: draws it and doesn't animate.

render?ReactElement | ((props: object, state: object) => ReactElement)

Replace the host element or compose it with another component.

Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string

Forwarded to the part.

All 101 shaders