CSS Houdini APIs
Extending CSS From JavaScript
CSS Houdini is a set of low-level APIs that let you hook into the browser's rendering engine. Instead of waiting for browser vendors to implement new CSS features, you can write them yourself: custom paint functions, typed properties with animation support, and custom layout algorithms.
Not all Houdini APIs have shipped in all browsers. But the ones that have — particularly @property and the Paint API — solve real production problems today.
Think of Houdini as opening the hood of the rendering engine. Normally, you write CSS declarations and the browser does everything else — parsing, painting, layout — behind closed doors. Houdini gives you hooks at specific stages: "Here, you paint this element." "Here, you define how these children are laid out." You're extending the engine, not working around it.
Properties and Values API (@property)
The most production-ready Houdini API. It lets you define custom properties with types, initial values, and inheritance behavior:
@property --hue {
syntax: '<number>';
inherits: false;
initial-value: 240;
}
@property --progress {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
Enabling Custom Property Animation
Without @property, custom properties are strings — the browser can't interpolate between them:
/* Without @property — no animation */
:root { --color: red; }
.box {
background: var(--color);
transition: --color 0.5s; /* Nothing happens — browser can't interpolate strings */
}
.box:hover { --color: blue; }
/* With @property — smooth animation */
@property --color {
syntax: '<color>';
inherits: false;
initial-value: red;
}
.box {
background: var(--color);
transition: --color 0.5s; /* Smooth color transition */
}
.box:hover { --color: blue; }
Animating Gradients
The classic impossible-with-CSS problem, solved:
@property --gradient-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.card {
background: linear-gradient(var(--gradient-angle), #3b82f6, #8b5cf6);
transition: --gradient-angle 0.8s ease;
}
.card:hover {
--gradient-angle: 180deg;
}
/* Animated gradient border */
@property --border-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.fancy-border {
background: conic-gradient(from var(--border-angle), #3b82f6, #ec4899, #3b82f6);
animation: spin 3s linear infinite;
}
@keyframes spin {
to { --border-angle: 360deg; }
}
CSS Paint API
The Paint API lets you write JavaScript that draws directly to an element's background, border, or mask — like a programmable background-image:
// paint-worklet.js
class DotPattern {
static get inputProperties() {
return ['--dot-color', '--dot-size', '--dot-spacing'];
}
paint(ctx, size, props) {
const color = props.get('--dot-color').toString() || '#ddd';
const dotSize = parseInt(props.get('--dot-size')) || 4;
const spacing = parseInt(props.get('--dot-spacing')) || 20;
ctx.fillStyle = color;
for (let x = 0; x < size.width; x += spacing) {
for (let y = 0; y < size.height; y += spacing) {
ctx.beginPath();
ctx.arc(x, y, dotSize / 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
registerPaint('dot-pattern', DotPattern);
/* Register the worklet */
/* In JavaScript: CSS.paintWorklet.addModule('paint-worklet.js'); */
.hero {
background: paint(dot-pattern);
--dot-color: oklch(0.85 0.02 240);
--dot-size: 3;
--dot-spacing: 24;
}
.hero:hover {
--dot-spacing: 16; /* Dots get denser on hover */
}
The Paint API runs in a worklet — a separate thread with no DOM access. You can't read element content, query the DOM, or use most Web APIs. Your paint function receives only the canvas context, element size, and declared CSS custom properties. Think of it as a pure rendering function.
Paint API Use Cases
- Generative backgrounds: Noise, patterns, mesh gradients
- Dynamic borders: Squiggly lines, hand-drawn effects
- Placeholder backgrounds: Shimmer effects for loading states
- Data visualization: Mini charts as backgrounds
Layout API (Experimental)
The Layout API lets you define custom layout algorithms — your own display values:
// masonry-layout.js
class MasonryLayout {
static get inputProperties() {
return ['--masonry-gap'];
}
async intrinsicSizes() { /* Define min/max content sizes */ }
async layout(children, edges, constraints, styleMap) {
const gap = parseInt(styleMap.get('--masonry-gap')) || 16;
const columns = Math.floor(constraints.fixedInlineSize / 300);
const columnWidth = (constraints.fixedInlineSize - gap * (columns - 1)) / columns;
const columnHeights = new Array(columns).fill(0);
const childFragments = await Promise.all(
children.map(child =>
child.layoutNextFragment({ fixedInlineSize: columnWidth })
)
);
for (const fragment of childFragments) {
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
fragment.inlineOffset = shortestColumn * (columnWidth + gap);
fragment.blockOffset = columnHeights[shortestColumn];
columnHeights[shortestColumn] += fragment.blockSize + gap;
}
return { childFragments, autoBlockSize: Math.max(...columnHeights) };
}
}
registerLayout('masonry', MasonryLayout);
.gallery {
display: layout(masonry);
--masonry-gap: 16;
}
The Layout API is still experimental and has limited browser support. For masonry layouts today, use CSS Grid with grid-template-rows: masonry (Firefox only behind a flag) or JavaScript-based solutions. The API is included here for awareness — it represents where CSS is heading.
Browser Support Status
| API | Chrome | Firefox | Safari | Status |
|---|---|---|---|---|
@property | Yes | Yes | Yes | Production ready |
| Paint API | Yes | No | No | Chromium only |
| Layout API | Experimental | No | No | Not ready |
| Animation Worklet | Experimental | No | No | Not ready |
| Typed OM | Yes | Partial | Partial | Mostly ready |
| What developers do | What they should do |
|---|---|
| Using @property for all custom properties Regular custom properties are simpler and more flexible. @property adds constraints that aren't always needed. | Only use @property when you need type validation, animation, or explicit inheritance control |
| Expecting the Paint API to work in Firefox and Safari Cross-browser support is incomplete. Always provide a fallback background for non-Chromium browsers. | Paint API is Chromium-only. Use progressive enhancement with CSS fallbacks. |
| Trying to access the DOM inside a paint worklet Worklets run in a separate thread for performance. They receive only the canvas context, dimensions, and CSS properties. | Worklets have no DOM access — pass data via CSS custom properties only |
| Overcomplicating with Houdini when CSS already has a solution Most CSS needs are met by modern CSS. Houdini is for genuinely novel visual effects. | Use Houdini only when CSS can't express what you need (animated gradients, custom paint, typed properties) |
- 1@property is production-ready across browsers — use it for typed custom properties and gradient animation
- 2The Paint API is Chromium-only — always provide CSS fallbacks for other browsers
- 3Worklets run in separate threads with no DOM access — communicate via CSS custom properties
- 4Don't use Houdini when standard CSS can solve the problem — it adds complexity
- 5The Layout API is experimental — use CSS Grid masonry proposals or JS solutions for now