Advanced CSS

CSS Houdini & Paint Worklets for Dynamic Patterns

Use CSS Houdini's Paint API to create parameterized, GPU-native SVG-like backgrounds using JavaScript worklets.

11 min read Jan 2026 3 PDF downloads

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