ibevanmeenen.me/houdini - Nov 2018
Javascript
Javascript
Javascript
CSS
CSS
CSS
Javascript
Javascript
Javascript
CSS
CSS
BIG MAGIC BOX
MY.SCSS
RESULT
MY.CSS
PREPROCESSOR
BLINK
GECKO
EDGE
WEBKIT
...
RENDERING ENGINE
RESULT
MY.CSS
RESULT
MY.CSS
JAVASCRIPT ENGINE
V8
SPIDERMONKEY
...
RENDERING ENGINE
BLINK
GECKO
EDGE
WEBKIT
...
MY.JS
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
RENDERING ENGINE
JAVASCRIPT
LOGIC
Parser
DOM
CSSOM
Cascade
Layout
Paint
Composite
No Access
Limited JS Access
Full JS Access
RENDERING ENGINE
Flow of writing your own css feature/polyfill
Write your basic logic
But wait...
Checking the rest of the CSS for matching rules, and then only replace the rule as an inline style if it's the last matching rule. Wait, that won't work, because we have to account for specificity, so we'll have to manually parse each selector to calculate its specificity. Then we can sort the matching rules in specificity order from low to high, and only apply the declarations from the most specific selector. Oh and then there's @mediarules, so we'll have to manually check for matches there as well. And speaking of at-rules, there's also @supports — can't forget about that. And lastly we'll have to account for property inheritance, so for each element we'll have to traverse up the DOM tree and inspect all its ancestors to get the full set of computed properties. Oh, sorry, one more thing: we'll also have to account for !important, which is calculated per-declaration instead of per-rule, so we'll have to maintain a separate mapping for that to figure out which declaration will ultimately win.
What about...
JAVASCRIPT
LOGIC
Parser
DOM
CSSOM
Cascade
Layout
Paint
Composite
No Access
Limited JS Access
Full JS Access
RENDERING ENGINE
“Without low-level styling primitives, innovation will move at the pace of the slowest-adopting browser”
“We believe that allowing developers to extend CSS is good for the ecosystem”
“If a developer wanted an additional feature they could implement it themselves. E.g. if the developer wanted a new type of dashed border, they shouldn't have to wait for browsers to implement this”
No Access
Limited JS Access
Full JS Access
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
No Access
Limited JS Access
Full JS Access
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
Worklets
Parser
API
Typed ObjectModel
API (v2)
Font Metrics
API
TBD
Box Tree
API
Layout
API
Paint
API
Animation
API
Scroll Custom.
API
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
Properties And Values
API
Worklets
Parser
API
Typed ObjectModel
API (v2)
Font Metrics
API
TBD
Box Tree
API
Layout
API
Paint
API
Animation
API
Scroll Custom.
API
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
Properties And Values
API
Originally sold and used as CSS-Variables
but is actually meant as an extension point.
:root {
--theme-background: #46cdcf;
--theme-color: #fff;
}
body {
color: var(--theme-color);
background: var(--theme-background);
}
.a-button {
color: var(--theme-color);
}
...
*.css
const root = document.documentElement;
const switchTheme = (e) => {
switch(e.target.value) {
case 'dark':
setTheme('#111', '#fff');
break;
case 'light':
setTheme('#fff', '#111');
break;
case 'tomato':
setTheme('tomato', '#fff');
break;
case 'broken':
setTheme('thisIsNotAColor', 'thisIsAlsoNotAColor');
break;
}
};
const setTheme = (background, color) => {
root.style.setProperty('--theme-color', color);
root.style.setProperty('--theme-background', background);
};
*.js
:root {
--theme-background: #46cdcf;
--theme-color: #fff;
}
body {
color: var(--theme-color);
background: var(--theme-background);
}
body.fixed-color {
color: #000;
}
@media screen and (min-width: 40rem) {
body {
color: tomato;
background: #fff;
}
}
*.css
"--" as an prefix is used to prevent preprocessors of compiling custom properties.
const switchTheme = (e) => {
switch(e.target.value) {
...
case 'broken':
setTheme(10, 'thisIsNotAColor');
break;
}
};
:root {
--theme-background: #46cdcf;
--theme-color: #fff;
}
body {
color: var(--theme-color);
background: var(--theme-background);
}
*.css
*.js
Solution:
CSS.registerProperty({
name: '--theme-color',
syntax: '<color>',
inherits: true,
initialValue: '#fff'
});
CSS.registerProperty({
name: '--theme-background',
syntax: '<color>',
inherits: true,
initialValue: '#454468'
});
*.js
<number> // 0, 2, 4.1, ...
<length> // 0, px, rem, ...
<percentage> // 10%, 0.5%, ...
<length-percentage> // px, rem, %, ...
<color> // #fff, rgba(0, 0, 0, .5), ...
<url> // url()
<image> // url, image-list, gradient, ...
<integer> // 1, 2, ...
<angle> // deg, grad, rad, ...
<time> // s, ms
<resolution> // dpi, dpcm, ...
<transform-list> // scale(), translate(), ...
<custom-ident> // test, ground-level, ...
CSS.registerProperty({
name: '--theme-color',
syntax: '<color>',
inherits: true,
initialValue: '#fff'
});
CSS.registerProperty({
name: '--theme-background',
syntax: '<color>',
inherits: true,
initialValue: '#454468'
});
*.js
syntax: "<length> | <percentage>",
Accept both, not in calc() combinations
syntax: "<length-percentage>",
Accept both and combination in calc()
syntax="<length>+"
Accepts a list of length values
--prop: 20rem;
--prop: 10%;
--prop: calc(20rem * 10%);
--prop: 20rem;
--prop: 10%;
--prop: calc(20rem * 10%);
--prop: 20rem, 10vw, 1px;
CSS.registerProperty({
name: '--vertical-spacing',
syntax: '<length>',
inherits: true,
initialValue: '1rem'
});
*.js
.article {
--vertical-spacing: 5rem;
max-width: 20rem;
margin-top: var(--vertical-spacing); /* 5rem */
}
.article__fig {
margin-top: var(--vertical-spacing); /* 5rem */
}
.article__fig__img {
margin-top: var(--vertical-spacing); /* 5rem */
}
*.css
<article class="article">
<figure class="article__fig">
<img class="article__fig__img" src="/awesome-image.jpg" alt="Awesome Image">
...
*.html
.article {
--vertical-spacing: 5rem;
max-width: 20rem;
margin-top: var(--vertical-spacing); /* 5rem */
}
.article__fig {
margin-top: var(--vertical-spacing); /* 1rem */
}
.article__fig__img {
margin-top: var(--vertical-spacing); /* 1rem */
}
*.css
CSS.registerProperty({
name: '--vertical-spacing',
syntax: '<length>',
inherits: false,
initialValue: '1rem'
});
*.js
<article class="article">
<figure class="article__fig">
<img class="article__fig__img" src="/awesome-image.jpg" alt="Awesome" >
...
*.html
body {
color: var(--theme-color);
background: var(--theme-background);
}
*.css
CSS.registerProperty({
name: '--theme-color',
syntax: '<color>',
inherits: true,
initialValue: '#fff'
});
CSS.registerProperty({
name: '--theme-background',
syntax: '<color>',
inherits: true,
initialValue: '#454468'
});
*.js
body {
color: var(--theme-color);
background: var(--theme-background);
transition: --theme-color .3s ease,
--theme-background .3s ease;
}
*.css
if (CSS.registerProperty) {
CSS.registerProperty({
name: '--theme-color',
syntax: '<color>',
...
*.js
"Big day for Houdini at #TPAC: We agreed to write the first *declarative* API,
informed by the imperative Properties & Values API."
@property --highlight-color {
syntax: "<color>";
inherits: true;
initial-value: #fff;
}
*.css
CSS.registerProperty({
name: '--theme-color',
syntax: '<color>',
inherits: true,
initialValue: '#fff'
});
*.js
“Allows you to write a paint function which allows us to draw directly into an elements background, border, or content.”
The Worklet is a lightweight version of Web Workers and gives developers access to low-level parts of the rendering pipeline.
With Worklets, you can run JavaScript and WebAssembly code to do graphics rendering or audio processing where high performance is required.
Worklets
Parser
API
Typed ObjectModel
API (v2)
Font Metrics
API
TBD
Box Tree
API
Layout
API
Paint
API
Animation
API
Scroll Custom.
API
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
Properties And Values
API
YES
YES
NO
YES
NO
YES
registerPaint('my-module-name', class {
// Do awesome stuff
});
my-module.paint-module.js
By writing your own module
await CSS.paintWorklet.addModule('my-module.paint-module.js');
*.js
And plug them in to your desired worklet
Promise.all([
window.paintWorklet.addModule('my-module.paint-module.js'),
window.layoutWorklet.addModule('my-module.layout-module.js')
]).then(() => {
// Both modules are loaded, let's do some stuff
});
*.js
Wait for multiple modules to be loaded if needed
Let's draw a circle
registerPaint('circle', class {
static get inputProperties() {}
paint(ctx, size, properties) {}
});
circle.paint-module.js
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('circle.paint-module.js');
}
*.js
registerPaint('circle', class {
static get inputProperties() {}
paint(ctx, size, properties) {}
});
circle.paint-module.js
if ('paintWorklet' in CSS) {
CSS.paintWorklet.addModule('circle.paint-module.js');
}
*.js
registerPaint('circle', class {
static get inputProperties() {}
paint(ctx, size, properties) {}
});
circle.paint-module.js
.circle-btn {
background-image: paint(circle);
}
*.css
registerPaint('circle', class {
static get inputProperties() {}
paint(ctx, size, properties) {
const x = size.width / 2;
const y = size.height / 2;
const radius = Math.min(x, y);
ctx.fillStyle = '#454468';
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
});
circle.paint-module.js
.circle-btn {
background-image: paint(circle);
}
*.css
Add a dynamic circle color
.circle-btn {
--circle-color: #454468;
background: paint(circle);
transition: --circle-color 1s ease;
}
.circle-btn:hover {
--circle-color: tomato;
}
*.css
CSS.registerProperty({
name: '--circle-color',
syntax: '<color>',
inherits: false,
initialValue: '#fff'
});
*.js
registerPaint('circle', class {
static get inputProperties() {
return ['--circle-color'];
}
paint(ctx, size, properties) {
const fill = properties.get('--circle-color');
const x = size.width / 2;
const y = size.height / 2;
const radius = Math.min(x, y);
ctx.fillStyle = fill;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
});
circle.paint-module.js
.circle-btn {
--circle-color: #454468;
background: paint(circle);
transition: --circle-color 1s ease;
}
.circle-btn:hover {
--circle-color: tomato;
}
*.css
Circle where we click
CSS.registerProperty({
name: '--circle-x',
syntax: '<number>',
inherits: false,
initialValue: 0
});
const setPosition = (event, el) => {
const x = event.offsetX;
const y = event.offsetY;
el.style.cssText = `--circle-x: ${x}; --circle-y: ${y};`;
}
el.addEventListener('click', event => setPosition(event, el), false);
*.js
CSS.registerProperty({
name: '--circle-y',
syntax: '<number>',
inherits: false,
initialValue: 0
});
*.js
registerPaint('circle', class {
static get inputProperties() {
return ['--circle-color', '--circle-x', '--circle-y'];
}
paint(ctx, size, properties) {
const fill = properties.get('--circle-color');
const x = properties.get('--circle-x');
const y = properties.get('--circle-y');
const radius = Math.min(x, y);
ctx.fillStyle = fill;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
});
circle.paint-module.js
.circle-btn {
--circle-color: #454468;
background-image: paint(circle);
transition: --circle-color 1s ease;
}
.circle-btn:hover {
--circle-color: tomato;
}
*.css
.circle-btn {
--circle-color: #454468;
background-image: paint(circle);
transition: --circle-color 1s ease,
--circle-x 1s ease,
--circle-y 1s ease;
}
.circle-btn:hover {
--circle-color: tomato;
}
*.css
Fade out and expand
CSS.registerProperty({
name: '--animation-tick',
syntax: '<number>',
inherits: false,
initialValue: 0
});
*.js
.circle-btn {
--animation-duration: 1s;
--circle-color: #c6efeb;
}
.circle-btn.-animating {
--animation-tick: 1000;
background-image: paint(circle);
transition: --animation-tick var(--animation-duration) ease;
}
*.css
CSS.registerProperty({
name: '--animation-duration',
syntax: '<time>',
inherits: false,
initialValue: '0.3s'
});
const ANIM_CLASS = '-animating';
const setPosition = (event, el) => { ... }; // Set X and Y
const startAnimation = el => { el.classList.add(ANIM_CLASS); }; // Add animation class
const stopAnimation = el => { el.classList.remove(ANIM_CLASS); }; // Remove animation class
const setupElement = el => {
const duration = CSSNumericValue
.parse(getComputedStyle(el).getPropertyValue('--animation-duration'))
.to('ms').value || 1000;
let timeout;
el.addEventListener('click', event => {
stopAnimation(el);
setPosition(event, el);
startAnimation(el);
window.clearTimeout(timeout);
timeout = window.setTimeout(() => stopAnimation(el), duration);
}, false);
};
*.js
registerPaint('circle', class {
static get inputProperties() {
return ['--circle-color', '--animation-tick', '--animation-duration', '--circle-x', '--circle-y'];
}
paint(ctx, size, properties) {
const tick = properties.get('--animation-tick');
const duration = properties.get('--animation-duration').to('ms').value;
const fill = properties.get('--circle-color');
const x = properties.get('--circle-x');
const y = properties.get('--circle-y');
const radius = size.width * (tick / duration);
ctx.fillStyle = fill;
ctx.beginPath();
ctx.globalAlpha = 1 - (tick / duration);
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
}
});
circle.paint-module.js
.circle-btn {
--animation-duration: 1s;
--circle-color: #c6efeb;
}
.circle-btn.-animating {
--animation-tick: 1000;
background-image: paint(circle);
transition: --animation-tick var(--animation-duration) ease;
}
*.css
Awesome!
.circle-btn {
--animation-duration: 1s;
--circle-color: #c6efeb;
}
.circle-btn.-animating {
--animation-tick: 1000;
background-image: paint(circle);
transition: --animation-tick var(--animation-duration) ease;
}
*.css
(canvas, div, ::before, ::after, ...)
“ Gives you the ability to write their own layout algorithms in addition to the native algorithms user agents ship with today ”
EARLY STAGES
Let's create a
new layout
registerLayout('ratio', class {
static get inputProperties() {}
*intrinsicSizes() { /* Note: Not yet implemented */ }
*layout(children, edges, constraints, properties) {}
});
ratio.layout-module.js
if ('layoutWorklet' in CSS) {
CSS.layoutWorklet.addModule('ratio.layout-module.js');
}
*.js
ratio.layout-module.js
registerLayout('ratio', class {
static get inputProperties() {}
*intrinsicSizes() {}
*layout(children, edges, constraints, properties) {}
});
if ('layoutWorklet' in CSS) {
CSS.layoutWorklet.addModule('masonry.layout-module.js');
}
*.js
.my-element {
--aspect-ratio: 1.2;
display: layout(ratio);
}
*.css
registerLayout('ratio', class {
static get inputProperties() {}
*intrinsicSizes() {}
*layout(children, edges, constraints, properties) {}
});
ratio.layout-module.js
registerLayout('ratio', class {
static get inputProperties() {
return ['--aspect-ratio'];
}
*intrinsicSizes() {}
*layout(children, edges, constraints, properties) {
const ratio = properties.get('--aspect-ratio');
const autoBlockSize = constraints.fixedInlineSize * ratio;
return { autoBlockSize };
}
});
ratio.layout-module.js
Let's create a
masonry layout
registerLayout(
'masonry',
class {
static get inputProperties() {
return ['--masonry-padding', '--masonry-columns'];
}
*layout(children, edges, constraints, properties) {
const inlineSize = constraints.fixedInlineSize;
const padding = parseInt(properties.get('--masonry-padding'));
const columnValue = properties.get('--masonry-columns').toString();
// We also accept 'auto', which will select the BEST number of columns.
let columns = parseInt(columnValue);
if (columnValue === 'auto' || !columns) {
columns = Math.ceil(inlineSize / 350); // MAGIC NUMBER \o/.
}
// Layout all children with simply their column size.
const childInlineSize = (inlineSize - (columns + 1) * padding) / columns;
const childFragments = yield children.map(child => {
return child.layoutNextFragment({ fixedInlineSize: childInlineSize });
});
let autoBlockSize = 0;
const columnOffsets = Array(columns).fill(0);
childFragments.forEach(childFragment => {
// Select the column with the least amount of stuff in it.
const min = columnOffsets.reduce(
(acc, val, idx) => {
if (!acc || val < acc.val) {
return { idx, val };
}
return acc;
},
{ val: +Infinity, idx: -1 }
);
childFragment.inlineOffset = padding + (childInlineSize + padding) * min.idx;
childFragment.blockOffset = padding + min.val;
columnOffsets[min.idx] = childFragment.blockOffset + childFragment.blockSize;
autoBlockSize = Math.max(autoBlockSize, columnOffsets[min.idx] + padding);
});
return { autoBlockSize, childFragments };
}
}
);
masonry.layout-module.js
by Google Chrome Labs
“ This is a COMPLEX API and it uses foreign terminology. But we really want to give you, the web developer, the power that the rendering engines have when it comes to layout. Enjoy! :) ”
Properties And Values
API
Worklets
Parser
API
Typed ObjectModel
API (v2)
Font Metrics
API
TBD
Box Tree
API
Layout
API
Paint
API
Animation
API
Scroll Custom.
API
Parser
CSSOM
Cascade
Layout
Paint
Composite
DOM
MORE DEMOS!?
By Vincent De Oliveira
By Google Chrome Labs
github.com/nucliweb/awesome-css-houdini
A curated list of CSS Houdini resources.
...
“If a developer wanted an additional feature they could implement it themselves. E.g. if the developer wanted a new type of dashed border, they shouldn't have to wait for browsers to implement this”