and I work at Google.
If we grow the language in these few ways [that allow extensibility], then we will not need to grow it in a hundred other ways; the users can take on the rest of the task.
Guy Steele, Growing a Language, 1998 ACM OOPSLA
Might say yes to any, but must say no to all.
The Houdini TF is part of the CSSWG, dedicated to making CSS user-extensible.
Extension language is usually JS; occasionally simple parts may be "pure CSS".
html {
--header-color: #006;
--main-color: #06c;
--accent-color: #c06;
}
a { color: var(--main-color); }
a:visited { color: var(--accent-color); }
h1 {
color: var(--header-color);
background: linear-gradient(var(--main-color), #0000);
}
Custom Properties originally sold as "variables" (it's in the spec name!), for organizing repeated values throughout a spec.
Actually, custom properties are an extension point — they can take anything, and be processed by JS.
However, Level 1 spec is weak, optimized for "variables" usage, and intentionally simple, for easy first implementation.
Properties & Values spec starts fixing custom properties:
CSS.registerProperty({
name: "--stop-color",
syntax: "<color>",
initial: "transparent"
inherits: false,
});
.button {
--stop-color: red;
background: linear-gradient(var(--stop-color), black);
transition: --stop-color 1s;
}
.button:hover {
--stop-color: green;
}
Missing from current level is easy way to respond to a custom property, or changes to one.
In other words, custom properties still act like variables, not like author-created properties.
Lack is intentional, again, because simplicity makes it easier to implement correctly at first.
Level 2 will provide hooks into at least the "computed value" transformation, where most CSS magic happens.
CSS.registerProperty({
name: "--translate-x",
type: "<length-percentage>",
initial: "0px",
inherits: false
});
CSS.registerComputedValueHook({
inputProperties: ["--translate-*", "transform"],
outputProperties: ["transform"],
computedValue: function(input, output) {
const tx = input.get("--translate-x");
const ty = input.get("--translate-y");
const translate = new CSSTranslate(tx, ty);
output.set("transform", new CSSTransformValue(
translate,
...input.get('transform')));
}
});
#myElement {
--translate-x: 5px;
--translate-y: 10px;
}
.foobar {
--translate-x: 20px;
}
Previous example invoked CSSTransformValue
— this is a new Typed OM value.
Current CSS "Object Model" is string-based, which is dumb:
Real performance implications in hot code!
The Typed OM spec is a stopgap solution to this: https://drafts.css-houdini.org/css-typed-om/
Creates a bunch of objects to represent CSS values, rather than strings.
Uses a Map-like interface (actually a MultiMap) for style blocks.
This is a "bedrock" spec - all the Houdini specs depend on it.
let width = el.computedStyleMap.get("width");
el.styleMap.set("height", width.mul(.6));
let bgs = el.styleAttributeMap.getAll("background-image");
In some cases, not the most convenient: "rotate(45deg)"
is easier to write than new CSSRotation(CSS.deg(45))
Others similar convenience: "5px"
vs CSS.px(5)
Others, much easier: let newWidth = oldWidth.add(someLength);
, regardless of what the units are (rather than hand-parsing, doing math, and building a new string)
Hoping for future JS improvements that would make a v2 much easier: suffix constructors (let length = 5em;
), operator overloading (let newWidth = oldWidth + someLength;
), etc.
Speed gains, tho, are very real — animating many transforms becomes much faster in our tests.
Big recent changes to handle calc()
Need to handle future Houdini "custom units" and V&U4 "unit algebra"
calc(1*2 + 3)
== CSSMathSum(CSSMathProduct(1,2), 3)
Finally getting to the actual extensions!
Custom Paint is author-defined "paint sources", usable anywhere CSS expects an <image>
: background
, list-style
, border-image
, content
, etc.
For example, Lea Verou's plea for conic gradients wants syntax like:
padding: 5em; /* size */
background: conic-gradient(gold 40%, #f06 0);
border-radius: 50%; /* make it round */
CSS.registerPaint('circle', class {
// Syntax is too complex for v1 args, so just "*" for now
static inputArguments = ["*"];
paint(ctx, geom, props, args) {
// Determine the center point and cover radius.
const x = geom.width / 2;
const y = geom.height / 2;
const radius = Math.sqrt(x*x + y*y);
// Draw the arcs
let angleSoFar = CSS.deg(0);
for(arg of args) {
let stop = /* parse each arg into color/angle pair */;
ctx.fillStyle = stop.color;
ctx.beginPath();
ctx.arc(x, y, radius, angleSoFar.value, stop.angle.value, false);
ctx.fill();
angleSoFar = stop.angle;
}
}
});
padding: 5em; /* size */
background: paint(conic-gradient, gold 40%, #f06 0);
border-radius: 50%; /* make it round */
In future, when we have custom functions, a tiny bit more code will allow:
background: --conic-gradient(gold 40%, #f06 0);
Custom Paint (and most other new Houdini APIs) is run in a "worklet", a lighter version of a Web Worker.
Separate process, restricted environment, might be killed and restarted at any time. Can't save state between calls.
These restrictions necessary for efficiently calling into JS in the middle of layout/painting/etc.
Flexbox and Grid took so long, but we were so hungry for better layout, they're still useful.
Turnover cycle on new layout specs is way too long.
Like Guy Steele said, any individual new layout type might be reasonable, but we can't add all the useful layout types people might want to use.
Existing layout modes are responsive to "natural" size of children.
This is hard to measure today in JS.
Hacks include using an off-screen iframe to measure the laid-out size of elements without disturbing the current page.
(Read the Flexbox Layout Algorithm for examples.)
Existing layout modes know how to tell their parents about their own "natural" sizes, which enables auto-sizing, used everywhere.
Impossible to do well today without hacky/slow resize handlers.
(Read the CSS Sizing spec for details.)
Layout is heavily optimized today, and switching between browser and JS repeatedly is expensive.
Solution, like Custom Paint, is a "worklet" for your layout code.
Custom Layout is much more complicated and hard to write than Custom Paint.
Example I'm about to show is very preliminary, and over-simplified in dangerous ways, but shows roughly how layout code will look.
Better, more thorough examples upcoming in the spec.
registerLayout('block-like', class {
async layout(space, children, styleMap, breakToken) {
let blockOffset = 0;
const inlineSize = resolveInlineSize(space, styleMap);
const childFragments = [];
// ... snip ...
for (let child of children) {
let fragment = await child.doLayout(childSpace);
// Position the fragment in a block like manner and center
fragment.blockOffset = blockOffset;
fragment.inlineOffset =
Math.max(0, (inlineSize - fragment.inlineSize) / 2);
blockOffset += fragment.blockSize;
}
// ... snip ...
return {
blockSize: blockOffset,
inlineSize: inlineSize,
fragments: childFragments,
};
}});
Future Houdini specs (Custom Functions, Custom At-Rules, Custom Selectors) need to expose contents
Allowed grammars are intentionally limited; need to use "*"
for more complex things
Returns "low-level" values, same as the concepts in CSS Syntax
CSS.parseRule("& .foo { new-prop: value; }");
//==>
CSSQualifiedRule({
prelude: ["&", " ", ".", CSSKeywordValue("foo")],
body: [CSSDeclaration({
name: "new-prop",
body: [" ", CSSKeywordValue("value"), " "]
]
}
)
Arbitrary "value-level" extensions
Way more complicated than they look!
CSS.registerFunction({
"name": "--darken",
"type": "<color>",
"inputArguments": ["<color>", "<percentage>"],
}, (color, percent) => {
const newColor = color.toHSL();
newColor.lightness *= (1 - percent.value/100);
return newColor;
});
CSS.registerFunction({
"name": "--random",
"type": "<number>",
"per": "element",
}, () => CSS.number(Math.random()) );
:root { --main-color: red; }
.foo { color: --darken(var(--main-color), 20%); }
.bar { width: calc(--random() * 100%); }
--darken(currentcolor)
?
--foo(5%)
- 5% of what?attr()
?
toggle()
or var()
?
calc()
?
Arbitrary "rule-level" extensions
Typically don't rely on as much "live" information, making things easier.
@--svg #foo {
width: 200px; height: 100px;
@--circle {
cx: 50px; cy: 50px;
r: 2em;
}
}
.foo { background-image: --svg(#foo); }
/* ==> */
.foo { background-image: url("data:text/xml;..."); }
.foo {
color: red;
@--nest & > .bar {
text-decoration: underline;
}
/* ===> */
.foo { color: red; }
.foo > .bar { text-decoration: underline; }
Ask Me Anything - I Am A Spec Writer