I'm @snugug
This is the Houdini bunny
These slides are online!
https://talks.page.link/houdini-design-systems
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.
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
Nope
Kinda
WARNING Heads Up!
Some of Houdini is pretty stable and getting cross-browser implementations, some is still quite a bit away from being usable, and the landscape is currently in flux with Edge moving to Chromium. Most of what we're going to talk about today are the mostly stable bits, but you're going to need to use polyfills and get your progressive enhancement on in order to use this stuff today.
Core Magical Secrets
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 what make you powerful.
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 gives us the structure our CSS needs to cast the spells we want.
Yah, But What Can I DO With This?
The Rad
Custom Stuff
Please allow me to introduce you to ...
window.CSS
Custom Properties
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 space-separated list of length values<color>#
- Accepts a comma-separated list of color values
Custom Properties
In Your Design System
Register in CSS
Proposed CSS Syntax
@property --my-color {
syntax: '<color>';
}
JS Equivalent
if ('registerProperty' in CSS) {
window.CSS.registerProperty({
name: '--my-color',
syntax: '<color>',
});
}
Make CSS Variables Smarter
:root {
--main-blue: #466BB0;
// When this lands, we can drop the above!
@property --my-blue {
syntax: '<color>';
inherits: true;
initial-value: #466BB0;
}
// Make sure that --background is always valid!
@property --background {
syntax: '<image> | <color>';
inherits: false;
}
}
Make Real Custom Properties
:root {
// For use later with the Paint API
@property --theme-color {
syntax: 'blue|green|red|yellow|grey';
inherits: true;
initial-value: blue;
}
// For use later with the Layout API
@property --columns {
syntax: '<integer> | auto';
inherits: false;
initial-value: 2;
}
@property --padding {
syntax: '<integer>';
inherits: true;
initial-value: 0;
}
}
Paint API
- 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?
Of Course Not!
But with the Paint API you can, and it's really cool.
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
CSS.paintWorklet.addModule('js/circle.js');
Paint API
In Your Design System
A Rounded Tab Component
<rounded-tab>First</rounded-tab>
<rounded-tab>Second</rounded-tab>
<rounded-tab>Third</rounded-tab>
The first section! Isn't this cool?
The second section! Isn't this cool?
The third section! Isn't this cool?
<template id="tab-template">
<style>
.tab {
--tab-border-radius: calc(5px * var(--tab-multiplier));
--tab-border-offset: calc(30px * var(--tab-multiplier));
background: red;
border-image-outset: var(--tab-border-offset);
border-image-slice: 0 fill;
border-image-source: paint(TabBottom);
border-radius: var(--tab-border-radius) var(--tab-border-radius) 0 0;
display: inline-block;
font-size: 1em;
padding: 0.25em 0.5em;
box-sizing: border-box;
}
</style>
<div class="tab" part="body"><slot></slot></div>
</template>
CSS.registerProperty({
name: '--tab-multiplier',
syntax: '<number>',
inherits: true,
initialValue: 1
});
CSS.registerProperty({
name: '--tab-position',
syntax: 'left|right|middle',
inherits: false,
initialValue: 'middle'
});
}
CSS.paintWorklet.addModule('./tabs.js');
class RoundedTab extends HTMLElement {
constructor() {
super();
const template = document.getElementById('tab-template');
const content = template.content;
this.attachShadow({ mode: 'open' })
.appendChild(content.cloneNode(true));
}
}
customElements.define('rounded-tab', RoundedTab);
Paint-Powered Web Component Tabs
The first section! Isn't this cool?
The second section! Isn't this cool?
The third section! Isn't this cool?
LAYOUT API
- 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/ February 20, 2019 Editor's Draft
class myLayout {
// Properties to look for on calling element
static inputProperties = ['--foo'];
// Properties to look for on direct child elements
static childrenInputProperties = ['--bar'];
// Options for the Layout
static layoutOptions = { childDisplay: 'normal', sizing: 'block-like' };
// Determines how a box fits its content or fits in to our layout context
async intrinsicSizes(children, edges, styleMap) {
// children - Child elements of box being laid out
// edges - `LayoutEdges` of Parent Layout
// styleMap - Typed OM style map of box being laid out
}
async layout(children, edges, constraints, styleMap, breakToken) {
// children - Child elements of Parent Layout
// edges - `LayoutEdges` of Parent Layout
// constraints - `Layout Constraints` of Parent Layout
// styleMap - Typed OM style map of Parent Layout
// breakToken - Token (if paginating for printing for example) to resume layout at
}
}
// From https://drafts.css-houdini.org/css-layout-api/#example-13a91ee5 February 20, 2019 Editor's Draft
registerLayout('centered', class {
async intrinsicSizes(children, edges, styleMap)
// Get all the sizes!
const childrenSizes = await Promise.all(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.maxContentSize);
}, 0) + edges.inline;
// How small the box can be so that its content
// doesn't overflow
const minContentSize = childrenSizes.reduce((max, childSizes) => {
return Math.max(max, childSizes.minContentSize);
}, 0) + edges.inline;
return { maxContentSize, minContentSize };
}
// ...
// ...
async layout(children, edges, constraints, styleMap) {
// Determine our (inner) available size
const availableInlineSize = constraints.fixedInlineSize - edges.inline;
const availableBlockSize = constraints.fixedBlockSize ?
constraints.fixedBlockSize - edges.block : null;
const childFragments = [];
const childConstraints = { availableInlineSize, availableBlockSize };
// Build fragments inside the content area
const childFragments = await Promise.all(children.map((child) => {
return child.layoutNextFragment(childConstraints);
}));
// ...
// ...
// Start counting block positioning from the start of block edges
let blockOffset = edges.blockStart;
for (let fragment of childFragments) {
// Set fragment's block offset
fragment.blockOffset = blockOffset;
// Center the block inline
fragment.inlineOffset = Math.max(
edges.inlineStart,
(availableInlineSize - fragment.inlineSize) / 2);
// Add the fragment's block size to the offset to set the next below this one
blockOffset += fragment.blockSize + styleMap.get('--gap').value;
}
// Add edges back on at the end
const autoBlockSize = blockOffset + edges.blockEnd;
// Return the element's generated blockSize, and child fragments
return {
autoBlockSize,
childFragments,
};
}
});
LAYOUT API
In Your Design System
Animated Circular Navigation Menu
https://talks.page.link/houdini-design-systems
- houdini.glitch.me - Interactive Houdini Playground
- CSS Houdini Drafts
- Animation Working Group
- Google Houdini Demos
- Talk Source Code
- @snugug (that's me!)