CSSCSS Paint API (Houdini)

CSS Paint API (Houdini)

The Paint API, part of the CSS Houdini family, lets you register a small JavaScript "paint worklet" that draws directly into a space CSS would otherwise fill with a background or border image. Instead of pre-generating a static image asset for a decorative pattern, the browser calls your worklet's paint() method whenever it needs to paint that element, handing it a drawing context that works much like the <canvas> 2D API.
Registering a worklet
A worklet is a small, separate JavaScript module. It defines a class with a paint() method and registers it under a name with registerPaint().

JS
// dashed-border-worklet.js
class DashedBorderPainter {
  static get inputProperties() {
    // Custom properties this worklet wants to read from the
    // element it's painting — lets CSS parameterize the drawing.
    return ['--dash-color', '--dash-gap']
  }

  paint(ctx, size, properties) {
    const color = properties.get('--dash-color').toString().trim() || '#0066cc'
    const gap = parseFloat(properties.get('--dash-gap')) || 8

    ctx.strokeStyle = color
    ctx.lineWidth = 3
    ctx.setLineDash([gap, gap])
    ctx.strokeRect(4, 4, size.width - 8, size.height - 8)
  }
}

registerPaint('dashed-border', DashedBorderPainter)
Loading the worklet and using it from CSS

JS
// In your page's regular JavaScript
if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('/dashed-border-worklet.js')
}

CSS
.decorative-card {
  --dash-color: #cc6600;
  --dash-gap: 6;

  /* Use the registered worklet name just like an image */
  background-image: paint(dashed-border);
  padding: 1.5rem;
}
Custom properties are how CSS parameters get INTO the worklet
Because a worklet can declare inputProperties, ordinary custom properties become the parameter-passing mechanism between CSS and JavaScript. Different elements using the same worklet can look completely different just by setting different values for those custom properties — no separate worklet needed per variant.
Worked example: a custom-drawn decorative border pattern

A design system that needs a repeating hand-drawn-looking border, a halftone-dot pattern, or a generative noise texture behind a card traditionally has two options: ship a pre-rendered PNG/SVG asset (which doesn't adapt to the element's actual size or theme colors without generating multiple variants), or draw it with JavaScript onto a positioned canvas element sitting behind the content (extra DOM, extra layout bookkeeping). A paint worklet draws procedurally, at whatever size the element actually is, parameterized by ordinary CSS custom properties — closer to how an SVG pattern behaves, but with the full expressiveness of an imperative drawing API.

  • The worklet re-runs automatically whenever the element is resized or an input custom property it declared changes — no manual redraw logic to write.

  • Drawing happens off the main JS thread in a dedicated paint worklet context, so it does not block script execution the way a canvas redraw loop can.

  • Because it plugs into background-image, it composes with everything else background supports — position, size, repeat.

Niche, advanced, and genuinely optional for most projects
The vast majority of visual effects — gradients, shadows, patterns — are achievable with plain CSS, and should be reached for first. The Paint API earns its complexity specifically for design-system-level custom visual effects that plain CSS truly cannot express and that would otherwise require generating and maintaining static background image assets. Most teams will never write a paint worklet, but it's a fascinating capability to know exists when that specific need comes up.
Next
The other side of Houdini — reading and writing CSS values as structured objects: [CSS Typed Object Model](/css/typed-om).