I'm Tab Atkins

and I work at Google.

@tabatkins

xanthir.com

xanthir.com/talks/2017-06-16

Growing a Language

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

PDF, YouTube

Growing a Language

Might say yes to any, but must say no to all.

Houdini

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".

http://drafts.css-houdini.org/

Custom Properties

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

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.

Custom Properties

Properties & Values spec starts fixing custom properties:

http://drafts.css-houdini.org/css-properties-values-api/

Custom Properties Example

CSS.registerProperty({
  name: "--stop-color",
  syntax: "<color>",
  initial: "transparent"
  inherits: false,
});

Custom Properties Example

.button {
  --stop-color: red;
  background: linear-gradient(var(--stop-color), black);
  transition: --stop-color 1s;
}
.button:hover {
  --stop-color: green;
}

Punted to Level 2

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.

Custom Properties 2 Example

CSS.registerProperty({
  name: "--translate-x",
  type: "<length-percentage>",
  initial: "0px",
  inherits: false
});

Custom Properties 2 Example

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')));
	}
});

Custom Properties Example

#myElement {
	--translate-x: 5px;
	--translate-y: 10px;
}

.foobar {
	--translate-x: 20px;
}

Typed OM

Previous example invoked CSSTransformValue — this is a new Typed OM value.

Current CSS "Object Model" is string-based, which is dumb:

  1. Browser holds CSS values in specialized data structures
  2. Serializes it to a string when you ask for a property value.
  3. You parse the string back into specialized data structures.
  4. When done, you build a string out of your values again.
  5. Browser parses the string into specialized data structures.

Real performance implications in hot code!

Typed OM

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.

Typed OM

let width = el.computedStyleMap.get("width");
el.styleMap.set("height", width.mul(.6));

let bgs = el.styleAttributeMap.getAll("background-image");

Typed OM

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)

Typed OM

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.

Typed OM calc()

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)

Custom Paint

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.

https://drafts.css-houdini.org/css-paint-api/

Custom Paint Example

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 */

Custom Paint Example

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;
		}
	}
});

Custom Paint Example

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);

Diversion: Worklets

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.

Custom Layout

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.

https://drafts.css-houdini.org/css-layout-api/

Layout "below"

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.)

Layout "above"

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 hard

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 Example

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.

Custom Layout Example

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,
		};
	}});

LET'S

GET

SPECULATIVE

Custom Parser

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

https://wicg.github.io/CSS-Parser-API/

Custom Parser

CSS.parseRule("& .foo { new-prop: value; }");
//==>
CSSQualifiedRule({
  prelude: ["&", " ", ".", CSSKeywordValue("foo")],
  body: [CSSDeclaration({
    name: "new-prop",
    body: [" ", CSSKeywordValue("value"), " "]
    ]
  }
)

Custom Functions

Arbitrary "value-level" extensions

Way more complicated than they look!

darken()

CSS.registerFunction({
  "name": "--darken",
  "type": "<color>",
  "inputArguments": ["<color>", "<percentage>"],
  }, (color, percent) => {
    const newColor = color.toHSL();
    newColor.lightness *= (1 - percent.value/100);
    return newColor;
  });

random()

CSS.registerFunction({
  "name": "--random",
  "type": "<number>",
  "per": "element",
  }, () => CSS.number(Math.random()) );

Custom Functions

:root { --main-color: red; }
.foo { color: --darken(var(--main-color), 20%); }
.bar { width: calc(--random() * 100%); }

Custom Function Complications

Custom At-Rules

Arbitrary "rule-level" extensions

Typically don't rely on as much "live" information, making things easier.

@--svg

@--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;..."); }

@--nest

.foo {
  color: red;
  @--nest & > .bar {
    text-decoration: underline;
  }
/* ===> */
.foo { color: red; }
.foo > .bar { text-decoration: underline; }

The End

Ask Me Anything - I Am A Spec Writer

@tabatkins

xanthir.com

http://xanthir.com/talks/2017-06-16