Serg Hospodarets
Engineering and technology leader, speaker, Web and Cloud infrastructure enthusiast.
/* declaration */
--VAR_NAME: <declaration-value>;
/* usage */
var(--VAR_NAME [, <fallback-if-not-defined-value>])
/* root element selector (global scope) */
/* usually <html> */
:root {/* make available for whole the app */
/* CSS variables declarations */
--main-color: #ff00ff;
--main-bg: rgb(200, 255, 255);
}
body {
/* variable usage */
color: var(--main-color);
background-color: var(--main-bg, #fff);
}
"--" prefix picked to prevent preprocessors to compile Custom Properties
const breakpointsData =
document.querySelector('.breakpoints-data');
// GET
const phone = getComputedStyle(breakpointsData)
.getPropertyValue('--phone');
// SET
breakpointsData.style
.setProperty('--phone', 'custom');
.breakpoints-data {
--phone: 480px;
--tablet: 800px;
}
Custom CSS Properties by default are:
Property | Value definition field | Example value |
---|---|---|
text-align | left | right | center | justify | center |
padding-top |
<length> | <percentage> | 5% |
border-width | [ <length> | thick | medium | thin ]{1,4} | 2px medium 4px |
CSS
Properties
& Values
API
CSS
Typed
OM
CSS
Parser
API
Font
Metrics
API
Worklets
CSS
Layout
API
CSS
Paint
API
CSS
Animation
Worklet
API
CSS
Properties
& Values
API
CSS
Typed
OM
// CSS -> JS
const styleMap = document.body.computedStyleMap();
console.log( styleMap.get('font-size') );
// CSSUnitValue {value: 14, unit: "px"}
// JS -> JS
console.log( new CSSUnitValue(5, "px") );
// CSSUnitValue {value: 5, unit: "px"}
// JS -> CSS
// set style "transform: translate(50px, 80%);"
document.body.attributeStyleMap
.set('transform',
new CSSTransformValue([
new CSSTranslate(
new CSSUnitValue(50, 'px'), new CSSUnitValue(80, '%')
)]));
behind the “Experimental Web Platform features” flag in
CSS.registerProperty({
name: "--stop-color",
syntax: "<color>",
inherits: false,
initialValue: "black"
});
Without
With
Default: "*"
Supported Values:
"<length>"
"<number>"
"<percentage>"
"<length-percentage>"
"<color>"
"<image>"
"<url>"
"<integer>"
"<angle>"
"<time>"
"<resolution>"
"<transform-function>"
"<length> | <percentage>"
both, but not calc() combinations
"<length-percentage>"
both + calc() combinations of both types
"big | bigger"
accepts either value
"<length>+"
accepts a list of length values
CSS Custom Properties
should be animatable
since they are provided with types?
Let's try!
element.animate([
{cssProperty: 'fromValue'},
{cssProperty: 'toValue'}
], {
duration: timeInMs,
fill: 'none|forwards|backwards|both',
delay: delayInMs,
easing: 'linear|easy-in|cubic-bezier()...',
iterations: iterationCount|Infinity
});
rabbit.animate(
[
{ transform: "translateX(0)" },
{ transform: "translateX(115px)" }
],
{
duration: 1000, // ms
fill: "forwards", // stay at the end
easing: "easy-in-out"
}
);
@keyframes rabbitMove {
0% {
transform: translateX(0);
}
100% {
transform: translateX(115px);
}
}
.rabbit {
animation: rabbitMove 1s ease-in-out;
animation-fill-mode: forwards;
}
Polyfill is available 🎉
CSS
JavaScript
Browser
CSS Custom Properties
CSS Property Types
Typed OM API
CSSOM
?
Pixel
rendering
pipeline
Houdini’s goal is to expose browser APIs to allow web developers to hook up their own code into the CSS engine.
It’s probably not unrealistic to assume that some of these code fragments will have to be run every. single. frame.
- similar to Web and Service Workers
- have a separate thread
- don't interact with DOM directly
- focus on performance
- use the latest JS additions:
ES Modules, classes, Promises/await
- triggered when needed and possible
The paint stage of CSS is responsible for painting the background, content and highlight of a box (based on the box’s size and computed style). The API allows to paint a part of a box in response to size / computed style changes with an additional <image> function.
- background-image
- border-image
- list-style-image
- content
- cursor
Custom image can be paint on every browser paint action.
Applicable for:
CSS
Paint
API
/* CSS (add CUSTOM property and register a paint directive) */
.multi-border {--border-top-width: 10; border-image: paint(border-colors);}
// JS (providing a type for the CUSTOM property to pass to JS)
CSS.registerProperty({
name: '--border-top-width',
syntax: '<number>',
inherits: false,
initialValue: 0,
});
// add a Worklet
CSS.paintWorklet.addModule('border-colors.js');
// WORKLET "border-colors.js"
registerPaint('border-colors', class BorderColors {
static get inputProperties() { return ['--border-top-width']; }
paint(ctx, size, styleMap) {
// get width and Custom Property value
const elWidth = size.width;
const topWidth = styleMap.get('--border-top-width').toString();
// draw a rectangle (border-top)
ctx.fillStyle = 'magenta';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(elWidth, 0); ctx.lineTo(elWidth, topWidth);
ctx.lineTo(0, topWidth); ctx.lineTo(0, 0);
ctx.fill();
}
});
Browsers are smart and trigger paint only when and where needed (demo)
Activate "Rendering pane" -> "Paint Flashing" to highlight paints
- to create own layout algorithms
- having access to set constraints, behaviour, boundaries
- interact with blocks, fragments and even texts
/* CSS */
.center {
display: layout(centering);
}
CSS
Layout
API
/* CSS */
.photos {
display: layout(masonry);
}
// JS (layout worklet)
registerLayout('masonry',
class {
*layout(space, children,
styleMap, edges) {/*...*/}
});
Sticky Header implemented in the stable version of Chrome using Houdini task force achievements
Web Animation API based, makes possible to run in own dedicated thread isolated from the main for performance
("best-effort" basis- runs on every frame, up to the frame deadline)
Mostly scroll-linked animations and effects:
- sticky elements
- smooth scroll animations
- scroll snapping
- scroll up bar
Worklet and Web Animation API polyfills are available 🎉
CSS
Animation
Worklet
API
Pixel
rendering
pipeline
Changing does not trigger any geometry changes or painting
Carried out by the compositor thread with the help of the GPU.
Property changes which can be handled by the compositor alone (not changing scroll offset/layout)
Promote with "will-change"
<!-- HTML (scripts) -->
<!-- Polyfill checks and loads (if needed)
both Web Animation API and Animation Worklet polyfills -->
<script src="polyfill/anim-worklet.js"></script>
<script src="animator.js"></script>
/* animator.js (load a Worklet module) */
window.animationWorkletPolyfill.addModule('worklet.js')
.then(()=> {
// onWorkletLoaded()
}).catch(console.error);
/* worklet.js - register and apply animations */
// Animators are classes registered in the worklet execution context
registerAnimator(
'scroll-position-animator',// animator name
class { // extends Web Animation
constructor(options) {
this.options = options;
}
// currentTime, KeyframeEffect and localTime concepts
// from Web Animation API
// animate function with animation frame logic
animate(currentTime, effect) {
// scroll position can be taken from option params
// const scrollPos = currentTime * this.options.scrollRange;
// each effect will apply the animation options
// from 0 to 100% scroll position in the scroll source
effect.children.forEach((children) => {
// currentTime is a Number,
// which represent the vertical scroll position
children.localTime = currentTime * 100;
});
}
});
/* animator.js (onWorkletLoaded() ) */
const scrollPositionAnimation = // animator instance
new WorkletAnimation(
'scroll-position-animator', // animation animator name
[ // animation effects
new KeyframeEffect(scrollPositionElement, [ // scroll position
{'transform': 'translateX(-100%)'}, // from
{'transform': 'translateX(0%)'} // to
],
{duration: 100, iterations: 1, fill: 'both'} // options
),
new KeyframeEffect(subscribeElement, [ // size and opacity
{'transform': 'scale(0.5)', 'opacity': 0}, // from
{'transform': 'scale(1)','opacity': 1} // to
],
{duration: 100, iterations: 1, fill: 'both'}) // options
],
new ScrollTimeline({ // animation timeline
scrollSource: document.querySelector('.page-wrapper'),
orientation: 'vertical'
})
);
scrollPositionAnimation.play(); // start and apply the animation
// animation options
{
pageTimeline: new ScrollTimeline({
scrollSource,
orientation: 'vertical',
endScrollOffset: '375px',
timeRange: 375
}
Bright Reality
- start using CSS Custom Properties
- register Custom Properties from JS (when available) for performance improvements and progressive enhancement
- play with Animation Worklet using the polyfill
Bright Future
- experiment with Paint Worklet in Chrome
- stay tuned with CSS Houdini Group and specs
(Worklets, Layout, Font Metrics, Typed OM APIs etc.)
Online slides: https://slides.com/malyw/houdini-short
By Serg Hospodarets
Today CSS Custom Properties are supported in all the major browsers. Now it’s time to do the next step- to have an ability to register new Custom Properties from JavaScript and setup the browser how to work with them (e.g. real CSS polyfills). They should work with the same performance as the native CSS properties, being animatable and aligned with CSSOM. Custom Properties can be used as a bridge between CSS and JavaScript. Houdini Task force introduces specs and JavaScript Worklets to expose the interaction with previously fully internal browser rendering mechanisms (during Paint, Layout, Composite stages) and Animations thread. All this brings Front-End development to the next level, parts of which are already available for the developers.
Engineering and technology leader, speaker, Web and Cloud infrastructure enthusiast.