What Is the Paint API?
The CSS Houdini Paint API (also called CSS Custom Paint) lets you define a JavaScript class that renders to a Canvas 2D context, then use it as a CSS image value: background-image: paint(myPainter). The browser calls your paint function whenever it needs to render the background — it's parameterized, GPU-native, and responds to CSS custom property changes without JavaScript in the main thread.
Registering a Paint Worklet
Paint worklets run in a separate thread. Register one from your main JS file: CSS.paintWorklet.addModule('/pattern-worklet.js'). In the worklet file, call registerPaint('my-pattern', class { ... }). The class must implement a paint(ctx, geometry, properties) method where ctx is a Canvas 2D rendering context.
// pattern-worklet.js
registerPaint('dots', class {
static get inputProperties() {
return ['--dot-color', '--dot-size', '--dot-spacing'];
}
paint(ctx, { width, height }, props) {
const color = props.get('--dot-color').toString().trim();
const size = parseFloat(props.get('--dot-size'));
const spacing = parseFloat(props.get('--dot-spacing'));
for (let x = spacing/2; x < width; x += spacing) {
for (let y = spacing/2; y < height; y += spacing) {
ctx.beginPath();
ctx.arc(x, y, size/2, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
}
}
});
CSS Custom Properties as Parameters
Declare inputProperties as a static getter returning the CSS custom property names your painter reads. These properties are automatically observed — when their values change (via JS, hover, etc.), the browser calls paint() again on the next frame. This enables zero-JavaScript pattern animations driven purely by CSS transitions on custom properties.
Animation via Custom Properties
Use @property to register CSS custom properties with types and transition support. Then animate them with CSS keyframes. The Paint Worklet automatically re-renders when the property value changes each frame — creating GPU-native animated patterns with no requestAnimationFrame needed.
@property --dot-size {
syntax: '<number>';
initial-value: 4;
inherits: false;
}
@keyframes pulse {
from { --dot-size: 3; }
to { --dot-size: 8; }
}
.element {
background-image: paint(dots);
animation: pulse 2s ease-in-out infinite alternate;
}
Progressive Enhancement Strategy
Paint Worklets are Chrome-only. Always provide a CSS data URI SVG fallback. The feature detection: if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('...') }. The CSS fallback is declared first; the paint() value overrides it only when supported. Users on Firefox and Safari see the SVG pattern; Chrome users get the dynamic paint version.
Performance vs SVG Data URIs
For static patterns, SVG data URIs are simpler and equally fast. Houdini Paint Worklets have an initial overhead (worklet module loading, registration). The performance advantage of Houdini emerges for: patterns that must animate continuously, patterns parameterized by many CSS properties that change frequently, and patterns that require Canvas 2D capabilities beyond SVG's declarative model (procedural noise, pixel manipulation).