What is Houdini?!
The objective of the CSS-TAG Houdini Task Force (CSS Houdini) is to jointly develop features that explain the “magic” of Styling and Layout on the web.
Magic
Practically, though, what does that mean?
Extending CSS via JS
so authors no longer have to wait a decade for standards bodies and browsers to do something new
But Wait! Can't We Do This Already!
Not Quite
- It's currently not possible to extend CSS with JS, only write JS that mimics CSS
- Actually polyfilling CSS, or introducing new features (like CSS Grids), is hard-to-impossible to do. Doubly so in a way that's not terrible for performance.
- Houdini will let authors tap in to the actual CSS engine, finally allowing them to extend CSS, and do so at CSS Engine speeds
Much like Service Workers are a low-level JavaScript API for the browser's cache Houdini introduces low-level JavaScript APIs for the browser's render engine
That's Cool
Nope
WARNING
This is very early days work in progress, nothing more. No compatible implementations exist. What little actually works only really does so in Chrome Canary. This deck will likely break if not viewed there. Syntax and semantics are likely to change. In fact, in the course of a year, one API has gone through 4ish incompatible API changes. Some of the examples provided are speculative based on previous and current working implementations and may not reflect final syntax. Terms and conditions apply. Not redeemable for cash. Your mileage may vary.
Worklets
Worklets are extension points for rendering engines
They're like Web Workers, but with a much smaller scope, can be parallelized, live on multiple threads, and most importantly, get called by the render engine, not us
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft
// From inside the browser's context
// Script gets loaded in the main thread, then sources are sent to Worklet threads
// Same script can be loaded in to multiple Worklet threads
window.demoWorklet.addModule('path/to/script.js');
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft
// worklet.addModule returns a Promise!
// Sometimes post-register work is done with loaded worklets, this makes it possible
Promise.all([
window.demoWorklet1.addModule('path/to/script1.js'),
window.demoWorklet2.addModule('path/to/script2.js'),
]).then(worklets => {
// Worklets have loaded and can be worked on!
});
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft
// The kind of worklet it is
registerDemoWorklet('name', class { // The name we'll call this worklet
// Each worklet can define different functions to be used
// These will be called by the render engine as needed
process(arg) {
// Stuff happens in here! What happens depends on the worklet
// Sometimes it'll return something
// Other times it'll work directly on the arguments
return !arg;
}
});
Worklets are the underlying foundation to which all of Houdini is based. They're the magic that makes it happen. They're Houdini's Secret Sauce
Typed OM
Typed OM exposes structure, beyond simple strings, for CSS Values. These then can be manipulated and retrieved in a more performant manner, and are part of the new CSSStyleValue class.
- CSSKeywordValue - CSS Keywords and other identifiers (like
inherit
) - CSSPositionValue - Position (
x
andy
) values - CSSTransformValue - A list of CSS transforms consisting of
CSSTransformComponent
includingCSSTranslation
,CSSRotation
,CSSRotation
,CSSScale
, andCSSSkew
- CSSUnitValue - Numeric values that can be expressed as a single unit (or a naked number or percentage)
- CSSMathValue - and its subclasses
CSSMathSum
,CSSMathProduct
,CSSMathMin
,CSSMathMax
,CSSMathNegate
, andCSSMathInvert
. Complicated numeric values (likecalc
,min
,max
, etc…)
.example {
background-position: center bottom 10px;
}
// From https://drafts.css-houdini.org/css-typed-om/ March 7, 2018 Editor Draft
let map = document.querySelector('.example').computedStyleMap();
map.get('background-position').x;
// CSSUnitValue { value: 50, unit: 'percent' }
map.get('background-position').y;
// CSSMathSum {
// operator: 'sum',
// values: [ // CSSNumericArray
// { value: -10, unit: 'px' }, // CSSUnitValue
// { value: 100, unit: 'percent' }, // CSSUnitValue
// ]
// }
The Typed OM is the glue to meaningfully connect our CSS and our worklets
Krazy Glue
Yah, But What Can I DO With This?
The Cool Custom Stuff
Please allow me to introduce you to ...
window.CSS
Custom Properties
Make Snozzberries Taste Like Snozzberries
Current Situation
.thing {
--my-color: green;
--my-color: url('not-a-color'); // It's just a variable! It doesn't know any better
color: var(--my-color);
}
But Then
window.CSS.registerProperty({
name: '--my-color',
syntax: '<color>', // Now it's def a color. That `url` junk is skipped!
});
Structure of a Registered Property
// From https://drafts.css-houdini.org/css-properties-values-api/ July 19, 2017 Editor's Draft
window.CSS.registerProperty({
name: '--foo', // String, name of the custom property
syntax: '<color>', // String, how to parse this property. Defaults to *
inherits: false, // Boolean, if true should inherit down the DOM tree
initialValue: 'black', // String, initial value of this property
})
The following are valid types for syntax
<length>
- Any valid length value<number>
- Number values<percentage>
- Any valid percentage<length-percentage>
- Any valid length or percentage value, any validcalc()
expression combining length and percentage<color>
- Any valid color value<image>
- Any valid image value<url>
- Any valid url value<integer>
- Any valid integer value<angle>
- Any valid angle value<time>
- Any valid time value<resolution>
- Any valid resolution value<transform-list>
- A list of valid transform-function values<custom-ident>
- Any valid custom-ident value
syntax
also allows for combiners
<length>
- A single length value<image> | <url>
- Accepts a single image or a single URLbig | bigger | BIGGER
- Accepts the ident "big", the ident "bigger", or the ident "BIGGER"<length>+
- Accepts a list of length values
Paint API
Pain, in CSS, but, like, for real
- Ever wanted to use
canvas
* as a background, a mask, or a border in CSS? - With the styling flexibility of an element?
- And the scalability of SVG?
That's what the Paint API does
paintWorklet
Class
// From https://drafts.css-houdini.org/css-paint-api/ January 28, 2018 Editor's Draft
class myPaint {
// Input properties from element to look for
static get inputProperties() { return ['--foo']; }
// Input arguments that can be passed to `paint` function
static get inputArguments() { return ['<color>']; }
// Alpha allowed?
static get alpha() { return true; }
paint(ctx, size, props, args) {
// ctx - drawing context
// size - size of the box being painted
// props - inputProperties
// args - array of passed-in arguments
// Paint code goes here.
}
}
Define - js/circle.js
registerPaint('circle', class {
static get inputProperties() { return ['--circle-color']; }
paint(ctx, size, props) {
// Change the fill color.
const circle = props.get('--circle-color'); // This is a CSSStyleValue!
// Determine the center point and radius.
const xCircle = size.width / 2;
const yCircle = size.height / 2;
const radiusCircle = Math.min(xCircle, yCircle) - 2.5;
// Draw the circle \o/
ctx.beginPath();
ctx.arc(xCircle, yCircle, radiusCircle, 0, 2 * Math.PI);
ctx.fillStyle = circle;
ctx.fill();
}
});
Import
window.CSS.paintWorklet.addModule('js/circle.js');
Now things get a little fuzzy
Animation API
Yo Dawg, I heard you like parallax
- Listen for user input, like scroll events!
- Style elements, based on that user input
- Do it all off of the main thread!
Animation API: Make Parallax Perform
animationWorklet
Class
// https://wicg.github.io/animation-worklet/ August 1, 2017 Draft Community Group Report
class myAnimator {
constructor(options) {
// Called when a new animator is instantiated
// Used to set stuff up for each use of an animator
}
animate(currentTime, effect) {
// currentTime - The current time from the defined timeline
// effect - Group of effects that this animation is working on
// Animation frame logic goes here.
}
}
// From https://wicg.github.io/animation-worklet/ August 1, 2017 Draft Community Group Report
registerAnimator('twitter-header', class {
// Called when new animator is instantiated
constructor(options) {
this.timing_ = new CubicBezier('ease-out');
}
// Internal function to perform effect
clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Animation Frame Logic
animate(currentTime, effect) {
const scroll = currentTime; // scroll is in [0, 1] range
// Drive the output group effect by setting its children local times
effect.children[0].localTime = scroll;
effect.children[1].localTime = this.timing_(clamp(scroll, 0, .5));
}
});
window.animationWorklet.addModule('twitter-header.js').then(_ => {
const workletAnim = new WorkletAnimation('twitter-header'),
// Worklet `effects`
[
new KeyFrameEffect( // effect.children[0] in worklet
$avatar, // Element to work on
// Scales down as we scroll up
[ { transform: 'scale(1)' }, { transform: 'scale(0.5)' } ], // Keyframes
{ duration: 1, iterations: 1 }, // Only want one frame per timeline tick
),
new KeyFrameEffect( // effect.children[1] in worklet
$header,
// Loses transparency as we scroll up
[ { opacity: 0 }, { opacity: 0.8 } ],
{ duration: 1, iterations: 1 },
),
],
// Worklet `curretTime` timeline
// New! Defines animation timeline whose time value depends on scroll position of a scroll container
// Work still being done for other input types, like touch and pointer input
new ScrollTimeline($scrollingContainer, {
timeRange: 1,
startScrollOffset: 0,
endScrollOffset: $header.clientHeight,
},
);
});
LAYOUT API
Turn Layout In To Tetris
- Literally, make your own
display
properties - Polyfill that awesome new layout spec you love!
- Everyone likes a good Masonry layout, add one without a performance hit!
layoutWorklet
Class
// From https://drafts.css-houdini.org/css-layout-api/ August 29, 2017 Collection of Interesting Ideas
// Intrinsic Sizes https://www.w3.org/TR/css-sizing-3/
class myLayout {
// Properties to look for on calling element
static get inputProperties() { return ['--foo']; }
// Properties to look for on direct child elements
static get childrenInputProperties() { return ['--bar']; }
// How children are displayed; either "block" to blockify like Flexbox and Grid
// or "normal" to let elements dictate
static get childDisplay() { return 'normal'; }
// Generator functions instead of normal functions to support async/parallel layout engines
// Determines how a box fits its content or fits in to our layout context
*intrinsicSizes(children, styleMap) {
// children - Child elements of box being laid out
// styleMap - Typed OM style map of box being laid out
// Intrinsic sizes code goes here.
}
*layout(space, children, styleMap, edges, breakToken) {
// space - `ConstraintSpace` for the box being laid out
// children - Child elements of the box being laid out
// styleMap - Typed OM style map of box being laid out
// edges - `LayoutEdges` of box being laid out
// breakToken - Token (if paginating for printing for example) to resume layout at
// Layout code goes here.
}
}
// From https://drafts.css-houdini.org/css-layout-api/#example-13a91ee5 August 29, 2017 Collection of Interesting Ideas
registerLayout('centered-stacked', class {
*intrinsicSizes(children, styleMap) {
// Get all the sizes!
const childrenSizes = yield children.map((child) => {
return child.intrinsicSizes();
});
// How large the box can be given unlimited space
// in order to fit its content with minimum unused
// space
const maxContentSize = childrenSizes.reduce((max, childSizes) => {
return Math.max(max, childSizes.maxContentContribution);
}, 0);
// How small the box can be so that its content
// doesn't overflow
const minContentSize = childrenSizes.reduce((max, childSizes) => {
return Math.max(max, childSizes.minContentContribution);
}, 0);
return {maxContentSize, minContentSize};
}
// ...
// ...
*layout(space, children, styleMap, edges) {
const inlineSize = resolveInlineSize(space, styleMap);
// Get content area inside edges
const availableInlineSize = inlineSize - edges.all.inline;
const availableBlockSize = resolveBlockSize(space, styleMap) - edges.all.block;
const childConstraintSpace = new ConstraintSpace({
inlineSize: availableInlineSize,
blockSize: availableBlockSize,
});
// Build fragments inside the content area
const childFragments = yield children.map((child) => {
return child.layoutNextFragment(childConstraintSpace);
});
// ...
// ...
// Start counting block positioning from the start of block edges
let blockOffset = edges.all.blockStart;
for (let fragment of childFragments) {
// Set fragment's block offset
fragment.blockOffset = blockOffset;
// Center the block inline
fragment.inlineOffset = Math.max(
edges.all.inlineStart,
(availableInlineSize - fragment.inlineSize) / 2
);
// Add the fragment's block size to the offset to set the next below this one
blockOffset += fragment.blockSize;
}
// ...
// ...
// Close off block size by adding edges
const autoBlockSize = blockOffset + edges.all.blockEnd;
// Resolve total block size
const blockSize = resolveBlockSize(
constraintSpace,
styleMap,
autoBlockSize
);
// Return the element's inlineSize, new blockSize, and child fragments
return {
inlineSize: inlineSize,
blockSize: blockSize,
childFragments: childFragments,
};
}
});
Representation of final block-like layout
Here come the fireworks, a working masonry layout using Layout API
👍
http://snugug.github.io/magic-tricks-with-houdini
- Interactive Houdini Playground
- CSS Houdini Drafts
- Animation Working Group
- Google Houdini Demos
- People who have helped me learn Houdini
- Tab Atkins - Spec hacker at Google
- Surma - Web Advocate and Engineer at Google
- Ian Kilpatrick - Software Engineer on Blink on Houdini
- @snugug (that's me!)