Lesson 27 of 30
New CSS features used to mean waiting years for a spec to land everywhere. Houdini takes a different approach: it hands JavaScript direct access to pieces of the browser's own rendering engine.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Why can't a plain, untyped custom property (e.g. `--progress: 50`) be smoothly animated with a CSS transition, even though `width` or `opacity` can be?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Normally, a new CSS capability follows the same path: someone proposes it, browsers implement it independently, and developers wait — often years — for it to become safe to rely on. CSS Houdini is a set of lower-level browser APIs that sidesteps that wait for specific problems by exposing pieces of the browser's own CSS rendering engine directly to JavaScript. Instead of simulating a visual effect in JS and hoping it looks close enough to native CSS, you register your own behavior and the browser treats it as a genuine, first-class part of the rendering pipeline — the same pipeline that paints borders, animates opacity, and lays out grid.
Houdini is a large, still-evolving collection of separate APIs, and most of it remains niche or experimental. This lesson covers the two pieces that are the most established and the most likely to show up in real work:
A plain custom property has no declared type — as far as the browser is concerned, --progress: 0 and --progress: 100 are just two arbitrary strings, and there's no way to interpolate between two arbitrary strings. That's why animating an untyped custom property doesn't work. The Properties API fixes this by letting you register a type:
@property --progress {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
.bar {
width: calc(var(--progress) * 1%);
transition: --progress 0.4s ease;
}
Once --progress is declared as a <number>, the browser knows how to smoothly interpolate between 0 and 100 the same way it interpolates width or opacity — the transition on .bar genuinely animates the property, not just snaps between values. (CSS.registerProperty() does the same thing from JavaScript, for cases where the type needs to be registered dynamically rather than declared statically in a stylesheet.)
The Paint API solves a different problem: drawing a custom visual pattern without exporting an SVG or image file. You register a "paint worklet" — a small piece of JavaScript that runs in its own isolated context and draws directly into a paint() value:
// paint-worklet.js
class CheckerPainter {
paint(ctx, size) {
const tile = 20;
for (let y = 0; y < size.height; y += tile) {
for (let x = 0; x < size.width; x += tile) {
if ((x / tile + y / tile) % 2 === 0) {
ctx.fillStyle = "rgba(0,0,0,0.06)";
ctx.fillRect(x, y, tile, tile);
}
}
}
}
}
registerPaint("checkerboard", CheckerPainter);
.panel {
background: paint(checkerboard);
}
ctx is a restricted 2D canvas-like context, and paint() re-runs it automatically whenever the element resizes — no manual redraw logic, no image asset to generate and ship. It's a programmatic, parameterized pattern generator that lives entirely in CSS and a small worklet file.
@property only matters when you specifically need a custom property to interpolate smoothly during a transition or animation, which untyped custom properties fundamentally cannot do.@property and CSS.registerProperty() are the same registration, two syntaxes. The at-rule form is declarative and lives in a stylesheet; the JS form is imperative and useful when the type needs to be computed or registered conditionally. Neither one is "the real API" with the other as a shortcut — they're equivalent entry points.CSS.paintWorklet.addModule("paint-worklet.js") in your page's JavaScript — forgetting this step is the most common reason a paint() background silently does nothing."Why can't you animate a CSS custom property with a plain transition?" is a real question that tests whether a candidate understands why untyped values can't interpolate — and knowing @property exists as the fix, rather than reaching for a JavaScript-driven animation as the only way out, signals genuine depth past the beginner explanation of custom properties.