Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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.
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:
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.
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():
// 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.
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:
.card {
background-image: paint(ripple, 3);
}Three errors account for most of the confusion when people first try this:
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.inputProperties. The browser only knows to re-invoke paint() when a declared dependency changes.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.
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:
-moz-element() and -webkit-canvas(), which is currently the most practical way to get near-universal supportThe 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.
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.
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.
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.

I am Muhammad Ali the founder and lead voice behind Techgory, a platform born out of a deep fascination with everyday technology and digital tools. Instead of relying on dense, confusing jargon, I focus on heavy research, hands-on testing, and breaking down complex tech into simple, actionable steps that anyone can understand.
Comments are closed.
[…] CSS work doesn’t need this level of custom rendering control. Ready to try it yourself? The step-by-step usage guide walks through registering and debugging your first worklet, or browse the worklet library if […]
[…] 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. […]