Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Shaders

Pro

CMYK 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.

Framework

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.

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

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.

Preview width

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.

Preview width

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.

Does it move?

Slowly: the blooms drift on long Lissajous paths, so the dots swell and shrink as color passes, and each plate sways on its own slow sine. speed scales it, speed={0} or paused holds the frame, and time draws one fixed moment, in seconds.

Will it slow my page down?

It costs under 1 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. On a slow GPU, a larger pitch doesn't help, as the cost is per pixel; a smaller box does.

API Reference

CmykHalftoneShader

data-slot="cmyk-halftone-shader"

PropType
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.

All 101 shaders