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