Extending CSS with Houdini
CSS Houdini is a family of low-level APIs that open the browser’s styling and layout engine to JavaScript. Instead of waiting for a new CSS feature to be specified and shipped, you can register custom paint routines, give custom properties real types, and (where supported) drive layout and animation from worklets. This reference lists the main Houdini APIs with their registration syntax, the inputs they consume, and how widely each is supported.
How it works
Most Houdini code runs in a worklet — a lightweight, isolated script context
loaded with addModule. Inside it you register a class:
// paint.js — loaded with CSS.paintWorklet.addModule('paint.js')
registerPaint('checker', class {
static get inputProperties() { return ['--tile-size']; }
paint(ctx, size, props) {
const t = parseInt(props.get('--tile-size'), 10) || 16;
for (let y = 0; y < size.height; y += t)
for (let x = 0; x < size.width; x += t)
if (((x / t) + (y / t)) % 2 === 0) ctx.fillRect(x, y, t, t);
}
});
Typed custom properties come from the main thread:
CSS.registerProperty({
name: '--tile-size', syntax: '<length>',
inherits: false, initialValue: '16px',
});
The worklet reads declared inputProperties, so animating --tile-size
re-runs the paint each frame.
API directory
| API | Registration call | Status |
|---|---|---|
| Paint Worklet | CSS.paintWorklet.addModule() | Production-ready in Chromium |
| Properties and Values | CSS.registerProperty() | Production-ready in Chromium |
| Typed OM | Read via attributeStyleMap | Production-ready in Chromium |
| Layout Worklet | CSS.layoutWorklet.addModule() | Experimental; limited support |
| Animation Worklet | CSS.animationWorklet.addModule() | Experimental; limited support |
Practical use: animating a typed custom property
Registering a custom property unlocks smooth CSS transitions and animations on values that are otherwise inert strings. For example, if you register --glow-size as a <length>:
CSS.registerProperty({
name: '--glow-size',
syntax: '<length>',
inherits: false,
initialValue: '0px',
});
You can now animate it in CSS:
.button {
--glow-size: 0px;
box-shadow: 0 0 var(--glow-size) blue;
transition: --glow-size 300ms ease;
}
.button:hover {
--glow-size: 20px;
}
Without registerProperty, transitioning --glow-size does nothing — the browser treats it as a string and does not know how to interpolate between 0px and 20px. With a registered <length> type, the transition works automatically.
Worklet isolation model
Worklets run in a separate thread from the main JavaScript context. They have no access to the DOM, the window object, or fetch. Data enters through inputProperties (a static list of custom property names the paint worklet will read) and inputArguments (values passed in the CSS function call like background: paint(name, 10px)). This isolation is intentional: it lets the browser run the paint callback on the compositor thread without risking main-thread blocking.
Tips and notes
- Always feature-detect —
if ('paintWorklet' in CSS) { ... }— and provide a CSS fallback. - A registered typed property (e.g.
<color>) becomes animatable; plain untyped--varscannot be smoothly transitioned. - Worklets are isolated: no DOM, no
window. Pass data through input properties and arguments only. - Paint and Properties/Values are the production-ready pair today; treat Layout and Animation worklets as experimental.
- The Typed OM (
attributeStyleMap,CSSStyleValue) is usable standalone and removes brittle string parsing of CSS values.