Skip to main content

CSS Houdini Explained

Answer​

CSS Houdini is a set of low-level APIs that expose parts of the CSS engine to developers, allowing them to extend CSS with new features, create custom properties with types, and build performant animations.

Houdini APIs​

Properties and Values API​

Define custom CSS properties with types, initial values, and inheritance.

// JavaScript registration
CSS.registerProperty({
name: "--my-color",
syntax: "<color>",
inherits: false,
initialValue: "blue",
});
/* CSS registration (preferred) */
@property --my-color {
syntax: "<color>";
inherits: false;
initial-value: blue;
}

@property --gradient-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}

@property --spacing {
syntax: "<length>";
inherits: true;
initial-value: 16px;
}

Animating Custom Properties​

Without @property, custom properties can't be animated smoothly:

/* ❌ Without @property - jumps between values */
.box {
--angle: 0deg;
background: linear-gradient(var(--angle), red, blue);
transition: --angle 1s;
}
.box:hover {
--angle: 180deg;
}

/* ✅ With @property - smooth animation */
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}

.box {
background: linear-gradient(var(--angle), red, blue);
transition: --angle 1s;
}
.box:hover {
--angle: 180deg;
/* Now smoothly animates! */
}

Syntax Values​

@property --my-prop {
/* Available syntax values */
syntax: "<length>"; /* 10px, 2em, etc. */
syntax: "<percentage>"; /* 50% */
syntax: "<color>"; /* red, #fff, etc. */
syntax: "<number>"; /* 42, 3.14 */
syntax: "<integer>"; /* 1, 2, 3 */
syntax: "<angle>"; /* 45deg, 1turn */
syntax: "<time>"; /* 500ms, 2s */
syntax: "<url>"; /* url('...') */
syntax: "<image>"; /* url() or gradient */
syntax: "<custom-ident>"; /* any identifier */
syntax: "*"; /* any value */

/* Combinations */
syntax: "<length> | <percentage>"; /* Either */
syntax: "<color>+"; /* One or more */
syntax: "<length>#"; /* Comma-separated list */
}

Gradient Animations​

@property --gradient-pos {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}

.gradient-move {
background: linear-gradient(
90deg,
blue var(--gradient-pos),
purple calc(var(--gradient-pos) + 50%),
red 100%
);
transition: --gradient-pos 2s ease;
}

.gradient-move:hover {
--gradient-pos: 50%;
}

Paint API​

// ripple.js
class RipplePainter {
static get inputProperties() {
return ["--ripple-color", "--ripple-x", "--ripple-y", "--ripple-size"];
}

paint(ctx, size, props) {
const color = props.get("--ripple-color").toString() || "rgba(0,0,0,0.2)";
const x = parseFloat(props.get("--ripple-x")) || 0;
const y = parseFloat(props.get("--ripple-y")) || 0;
const rippleSize = parseFloat(props.get("--ripple-size")) || 0;

ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, rippleSize, 0, 2 * Math.PI);
ctx.fill();
}
}

registerPaint("ripple", RipplePainter);
@property --ripple-size {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}

.button {
--ripple-x: 0;
--ripple-y: 0;
--ripple-color: rgba(255, 255, 255, 0.3);
--ripple-size: 0;
background-image: paint(ripple);
transition: --ripple-size 0.6s ease-out;
}

.button.rippling {
--ripple-size: 200px;
}

Layout API (Experimental)​

// masonry-layout.js
class MasonryLayout {
static get inputProperties() {
return ["--column-count", "--gap"];
}

async intrinsicSizes() {
// Return intrinsic size info
}

async layout(children, edges, constraints, styleMap) {
const columnCount = parseInt(styleMap.get("--column-count")) || 3;
const gap = parseInt(styleMap.get("--gap")) || 10;

// Custom layout logic here
const childFragments = await Promise.all(
children.map((child) => child.layoutNextFragment())
);

// Position children in masonry grid
// ...

return { childFragments };
}
}

registerLayout("masonry", MasonryLayout);
.gallery {
display: layout(masonry);
--column-count: 4;
--gap: 20px;
}

Animation Worklet​

// scroll-animation.js
class ScrollAnimation {
animate(currentTime, effect) {
// currentTime is scroll position (0-100%)
effect.localTime = currentTime * effect.getTimings().duration;
}
}

registerAnimator("scroll-linked", ScrollAnimation);
// Usage
await CSS.animationWorklet.addModule("scroll-animation.js");

const element = document.querySelector(".parallax");
const scrollTimeline = new ScrollTimeline({
source: document.scrollingElement,
scrollOffsets: [CSS.percent(0), CSS.percent(100)],
});

new WorkletAnimation(
"scroll-linked",
new KeyframeEffect(
element,
[{ transform: "translateY(0)" }, { transform: "translateY(-100px)" }],
{ duration: 1, fill: "both" }
),
scrollTimeline
).play();

Browser Support​

APIChromeFirefoxSafariEdge
@property✅✅✅✅
Paint API✅❌❌✅
Layout API🧪❌❌🧪
Animation Worklet⚠️❌❌⚠️

Feature Detection​

if ("registerProperty" in CSS) {
// @property supported
}

if ("paintWorklet" in CSS) {
// Paint API supported
}

if ("layoutWorklet" in CSS) {
// Layout API supported
}

if ("animationWorklet" in CSS) {
// Animation Worklet supported
}

Key Points​

  • Houdini exposes CSS engine internals to JavaScript
  • @property enables typed custom properties and smooth animations
  • Paint API allows custom CSS backgrounds via Canvas-like drawing
  • Layout API (experimental) enables custom layout algorithms
  • Animation Worklet enables performant scroll-linked animations
  • Use feature detection and provide fallbacks