How to Use CSS Houdini Worklets: A Step-by-Step Guide

If you’ve read about what CSS Houdini is — or want the quick plain-English version first — and you’re ready to actually wire a worklet into a real project, this is the practical half. It covers registration, passing data into a worklet, debugging the errors you’ll actually hit, and using worklets other people have already built.

Step 1: Confirm Your Environment Supports It

Paint worklets need two things: a secure context and a Chromium-based browser. “Secure context” means HTTPS in production, or localhost while developing — the API simply won’t register over plain HTTP or a file:// path. (For the full 2026 browser support breakdown across all Houdini APIs, see the pillar guide’s support table.) Before writing any worklet code, feature-detect it so your app degrades gracefully:

js
if ('paintWorklet' in CSS) {
  CSS.paintWorklet.addModule('/worklets/my-worklet.js');
} else {
  document.documentElement.classList.add('no-paint-worklet');
}

That no-paint-worklet class gives you a CSS hook to serve a static fallback, which matters a lot given Safari still doesn’t support the API.

Step 2: Register the Worklet

A worklet file is a small, self-contained JavaScript module — it can’t import anything from your main app bundle. Inside it, you extend a class with a paint() method and call registerPaint():

js
// ripple-worklet.js
class RipplePainter {
  static get inputProperties() {
    return ['--ripple-color', '--ripple-radius'];
  }

  paint(ctx, size, props) {
    const color = props.get('--ripple-color').toString() || '#3b82f6';
    const radius = parseInt(props.get('--ripple-radius')) || 40;
    const cx = size.width / 2;
    const cy = size.height / 2;

    ctx.strokeStyle = color;
    ctx.lineWidth = 2;
    for (let r = radius; r > 0; r -= 10) {
      ctx.globalAlpha = r / radius;
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI * 2);
      ctx.stroke();
    }
  }
}

registerPaint('ripple', RipplePainter);

inputProperties is the important part — it’s the explicit list of custom properties your worklet is allowed to read. If a property isn’t declared there, props.get() won’t return it, even if it’s set on the element. This is a deliberate sandboxing decision, not an oversight.

Step 3: Passing Data In

Worklets receive three things: a ctx (a PaintRenderingContext2D, a restricted version of Canvas 2D), a size object with width/height, and a properties map for anything listed in inputProperties. There’s no access to window, document, or fetch — the isolation is intentional, since worklets are designed to run off the main thread and potentially in parallel.

If you need input beyond custom properties — say, a numeric argument passed directly in the CSS paint() call — you can declare inputArguments and pass them like this:

css
.card {
  background-image: paint(ripple, 3);
}

Step 4: Debugging Common Errors

Three errors account for most of the confusion when people first try this:

  • “Failed to register” errors: usually means the worklet file was served over an insecure context, or the path in addModule() is wrong. Check the Network tab — if the file returns a 404, the worklet silently fails to draw with no visible error in some browsers.
  • The worklet draws once and never updates: happens when a custom property isn’t included in inputProperties. The browser only knows to re-invoke paint() when a declared dependency changes.
  • Nothing draws at all, no errors: double-check that the CSS property you’re targeting (background-image, border-image, mask-image) is one that accepts an <image> value — paint() won’t work on properties that don’t.

Chrome DevTools lets you inspect and override the custom property values on an element in the Styles pane, which is the fastest way to test a worklet’s response to different inputs without editing files.

Step 5: Using Community Worklets Instead of Writing Your Own

You don’t have to build every effect from scratch — the full worklet library and tools list has more options than what’s covered here. Several open collections exist:

  • CSSHoudini/paintlets — a gallery of downloadable, tweakable paint worklets on GitHub
  • GoogleChromeLabs/css-paint-polyfill — brings Paint Worklet support to Firefox and Safari via -moz-element() and -webkit-canvas(), which is currently the most practical way to get near-universal support
  • liyangguang/css-houdini — a small set of practical, ready-to-use worklets (tooltips, shapes) with copy-paste usage instructions

The common distribution pattern is to publish a worklet as an npm package and load it from a CDN like unpkg, since the HTTPS requirement makes CDN hosting the path of least resistance for sharing worklets across projects or demos like CodePen.

Using Houdini with React, Vue, or Next.js

Paint worklets aren’t tied to any framework  they’re a browser API, not a build-time feature. In a React or Vue app, the typical pattern is to call CSS.paintWorklet.addModule() once, on initial mount (a useEffect with an empty dependency array in React, or onMounted in Vue), guarded by the same 'paintWorklet' in CSS feature check. For Next.js or other SSR frameworks, make sure that registration call only runs client-side — wrap it in a check for typeof window !== 'undefined' or place it inside a client-only component, since CSS.paintWorklet doesn’t exist during server rendering.

Performance Notes

Worklets are designed to run off the main thread, which is the main performance case for using them over a JavaScript canvas overlay kept in sync manually. That said, a paint() function that does expensive per-pixel work on every repaint can still cost you — keep the drawing logic proportional to what’s actually changing, and avoid recalculating values inside paint() that could be computed once and cached.

Frequently Asked Questions

Why isn’t my worklet updating when I change a CSS variable? The variable almost certainly isn’t listed in inputProperties. The browser only tracks and re-triggers paint() for properties you’ve explicitly declared.

Can I use Houdini worklets with Tailwind CSS? Yes — Tailwind doesn’t interfere with paint() or @property, since both are plain CSS features. You’d typically still write the --custom-property: value declarations via an arbitrary-value utility or a small custom CSS block.

Do paint worklets work in a Web Component / Shadow DOM context? Yes, worklets apply globally once registered on the document — they aren’t scoped per shadow root, so a worklet registered anywhere is available inside shadow DOM styles too.

Is there a way to test Paint API support without shipping to production? Chrome, Edge, and other Chromium browsers support it by default from version 65 onward — no flag needed. For Firefox, enable layout.css.houdini.paint-worklets.enabled in about:config to test locally.

2 Comments

  1. […] That’s the whole loop: inputProperties tells the browser which custom properties your worklet cares about, paint() receives a canvas-like context plus the element’s size and property values, and the CSS paint() function wires it into any property that accepts an <image> — background-image, border-image, mask-image, and a few others. For a deeper walkthrough — debugging common errors, passing data into a worklet, and using it with React or Vue — see the full step-by-step usage guide. […]

Comments are closed.