shadcn.io is not affiliated with official shadcn/ui
Shaders
ProCMYK Halftone Shader for React
Big blooms of color printed in four process inks: cyan, magenta, yellow and black dot screens forming rosettes on warm paper, slightly out of register. For React with Base UI, when an editorial hero, a zine or a print shop wants a magazine page under a magnifying glass.
CMYK Halftone Shader prints a moving picture the way a magazine does. Soft blooms of cyan-blue, pink, yellow and violet drift slowly across the box, and the picture is separated into four process inks. Each ink is a screen of dots at its classic angle, cyan at 15°, magenta at 75°, yellow at 0° and black at 45°, with each dot sized by how much ink the picture needs there, so together they form the rosettes you see through a loupe. The inks overprint by multiplying on warm, faintly grained paper, and each plate drifts a pixel or so out of register. A WGSL fragment shader on WebGPU computes every dot each frame, for under 1 ms of GPU time.
Use it behind an editorial hero, a zine, a print shop or a design studio's page. For one ink on one screen, in plain CSS, use the Halftone background; for 1-bit pixel dithering, Dither Gradient Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows a CSS gradient of the same inks under a dot grid in browsers without WebGPU.
Installation
Usage
import {
CmykHalftoneShader,
} from "@/components/ui/cmyk-halftone-shader";<CmykHalftoneShader className="flex min-h-[32rem] items-center justify-center">
<div className="bg-[#f5f0e6]/80 px-8 py-6">
<h1 className="font-serif text-6xl font-bold text-neutral-900">Issue 04</h1>
</div>
</CmykHalftoneShader>CMYK Halftone Shader is a container: your content sits on top of the print, 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: it reads on the paper and the lighter tints, about three quarters of the box, but not on the dense blooms. For copy over them, use a paper panel like the bg-[#f5f0e6]/80 one above, which keeps near-black text above 9.5:1 everywhere.
Demos
Pop art
pitch={14} doubles the distance between dots, so each dot is big enough to see on its own, like a Lichtenstein panel, and intensity={1.3} lays down more ink, for bolder color.
Zine
cyanColor="#0078bf" and magentaColor="#ff48b0" swap in a risograph's blue and fluorescent pink, and misregister={3} lets the plates wander up to 3 pixels apart, for a hand-printed feel.
Performance
About 0.8 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. Each pixel checks the four nearest dots on each of the four plates, so pitch doesn't change the cost. Keep the default quality: the dots are a few pixels across and blur at half resolution. 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 blooms of the same inks under a fine dot grid, 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 print 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 blooms take a minute or more to drift across, so dots grow and shrink slowly, and the plates wander over seconds; measured over 6 seconds with WCAG's area test, and even in 32-pixel patches, it never flashes. Use dark text, on a paper panel over the blooms, as in Usage.
FAQ
What makes the rosettes?
Each ink's dot grid is turned to its own angle, the classic 15°, 75°, 0° and 45°. Where four grids at those angles overlap, their dots fall into small repeating flower shapes, the rosettes, instead of the coarse moiré you'd get from grids at nearly the same angle.
How is the color split into inks?
The picture is computed in RGB, then separated: black takes over only where it's darker than about 25%, as a printer's undercolor removal would, and cyan, magenta and yellow make up the rest. Each dot's area matches the ink it carries until neighboring dots touch and merge.
Can I change the inks and the paper?
Yes: cyanColor, magentaColor, yellowColor, keyColor and paperColor take any CSS color, var(--primary) included, and follow a theme switch. The inks multiply like real overprinting, so keep them lighter than they'd be on screen; dark inks quickly turn the overlaps muddy.
How do I change the dot size and the misregistration?
pitch is the distance between dots, from 4 to 16 CSS pixels: 7 reads as a magazine under a loupe, 14 as pop art. misregister is how far the plates drift apart, from 0 (perfect registration) to 3 pixels. intensity sets how much ink the blooms lay down.
API Reference
CmykHalftoneShader
data-slot="cmyk-halftone-shader"
cyanColor?string
The cyan ink. Any CSS colour. @default "#00a6d6".
- Default
"#00a6d6"
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.
intensity?number
How much ink the blooms lay down, from 0 (bare paper) to 2. @default 1.
- Default
1
interactive?boolean
Whether u.pointer follows the pointer. @default false.
keyColor?string
The black (key) ink. Any CSS colour. @default "#1a1a1a".
- Default
"#1a1a1a"
magentaColor?string
The magenta ink. Any CSS colour. @default "#e2007a".
- Default
"#e2007a"
misregister?number
How far the plates drift out of register, from 0 (perfect) to 3 CSS pixels. @default 1.
- Default
1
paperColor?string
The paper. Any CSS colour. @default "#f5f0e6".
- Default
"#f5f0e6"
paused?boolean
Holds the current frame. @default false.
pitch?number
The distance between dots, from 4 to 16 CSS pixels. @default 7.
- Default
7
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.
yellowColor?string
The yellow ink. Any CSS colour. @default "#ffe100".
- Default
"#ffe100"
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.
Related


Dither Gradient Shader
Digital & retro1-bit pixel dithering, free
- Pro


Marbling Shader
Ink, smoke & firePaper marbling for an editorial page


Light Leak Shader
Light & glassFilm leaks for a photographer's site, free
- Pro


Noise Contours Shader
Patterns & tilingsPoster-style enamel bands
- Pro


Pixel Sort Shader
Digital & retroCalm glitch art for an editorial hero
- Pro


Moire Shader
GeometryFine lines beating, ink on paper