and I work at Google.
https://www.xanthir.com/talks/2019-05-15
(Demos currently require Chrome, with the "Experimental Web Platform Features" flag turned on at chrome:flags.)
(Use left/right arrows to navigate.)
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>",
initialValue: "lime",
inherits: false,
});
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: "--slide-x",
type: "<length-percentage>",
initial: "0px",
inherits: false
});
* {
transform: translate(var(--slide-x), var(--slide-y));
}
CSS.registerComputedValueHook({
inputProperties: ["transform", "--slide-*"],
outputProperties: ["transform"],
computedValue: function(input, output) {
const tx = input.get("--slide-x");
const ty = input.get("--slide-y");
const translate = new CSSTranslate(tx, ty);
output.set("transform", new CSSTransformValue(
translate,
...input.get('transform')));
}
});
#my-element {
transform: rotate(10deg);
}
#my-element.foo {
--slide-x: 5px;
--slide-y: 10px;
}
#my-element.bar {
--slide-x: 20px;
}
Previous example invoked CSSTransformValue
— this is a new Typed OM value.
Current CSS "Object Model" is string-based, which is bad:
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 CSSRotate(CSS.deg(45))
Others similar convenience: "5px"
vs CSS.px(5)
Others, much easier: rot.angle = rot.angle.add(CSS.deg(10))
is much easier than parsing the transform, altering, serializing back to 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, if done properly (reusing objects).
calc(1*2 + 3)
== CSSMathSum(CSSMathProduct(1,2), 3)
Typed OM math functions allow "unit algebra", dividing lengths by lengths to get a number, etc. Back-ported to normal CSS in Values & Units 4.
Also back-ported "auto-rounding" of numbers into integers when necessary.
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.hypot(x, 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.
Tons more layouts we might want to add - Masonry, Tabs, Accordion, Constraint-Based...
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 = space.availableInlineSize;
const childFragments = [];
// ... snip ...
for (let child of children) {
let fragment = await child.doLayout(inlineSize);
// 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",
"inputArguments": ["<color>", "<percentage>"],
}, (color, percent) => {
const newColor = color.toHSL();
newColor.lightness *= (1 - percent.value/100);
return newColor;
});
CSS.registerFunction({
"name": "--random",
"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()
?
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;..."); }