Serg Hospodarets
Engineering and technology leader, speaker, Web and Cloud infrastructure enthusiast.
$font-size: 10px;
$font-family: Helvetica, sans-serif;
body {
font: $font-size $font-family;
}
.mark{
font-size: 1.5 * $font-size;
}
/* declaration */
--VAR_NAME: <declaration-value>;
/* usage */
var(--VAR_NAME)
/* 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);
}
"--" prefix picked to prevent preprocessors to compile Custom Properties
:root{
--main-color: #4d4e53;
--main-bg: rgb(255, 255, 255);
--logo-border-color: rebeccapurple;
--header-height: 68px;
--content-padding: 10px 20px;
--base-line-height: 1.428571429;
--transition-duration: .35s;
--external-link: "external link";
--margin-top: calc(2vh + 20px);
/* Valid CSS custom properties */
/* can be reused later in, say, JavaScript. */
--foo: if(x > 5) this.width = 10;
}
/* Default values */
.box{
/*--- Default values ---*/
/* 10px is used */
margin: var(--possibly-non-existent-value, 10px);
/* The --main-padding variable is used */
/* if --box-padding is not defined. */
padding: var(--box-padding, var(--main-padding));
}
/* Reuse values in other vars */
.box__highlight::after{
--box-text: 'This is my box';
/* Equal to --box-highlight-text:'This is my box with highlight'; */
--box-highlight-text: var(--box-text)' with highlight';
content: var(--box-highlight-text);
}
.common-values{
/* applies the value of the element’s parent. */
--border: inherit;
/* applies the initial value as defined in the CSS specification
(an EMPTY value, or NOTHING in some cases of CSS custom properties). */
--bgcolor: initial;
/* applies the INHERITED value if a property is normally inherited
(as in the case of CSS defined Custom Properties) or the initial value
if the property is normally not inherited. */
--padding: unset;
/* resets the property to the default value (user agent’s)
(an EMPTY value in the case of CSS custom properties). */
--animation: revert;
all: initial; /* resets all* CSS properties, EXCEPT CUSTOM PROPERTIES */
/* Future? */
--: initial; /* resets all CSS Custom Properties */
}
/* Separate values in CSS Custom Properties */
.transform {
--scale: scale(2);
--rotate: rotate(10deg);
transform: var(--scale) var(--rotate);
}
.transform:hover{
--rotate: rotate(90deg);
}
.transform {
transform: scale(2) rotate(10deg);
}
.transform:hover{
transform: scale(2) rotate(90deg);
}
:root {
--block-font-size: 1rem;
}
.block__highlight {
/* DOESN'T WORK */
font-size: var(--block-font-size)*1.5;
}
CSS calc(🤘) to the rescue (for values)
:root {
--block-font-size: 1rem;
}
.block__highlight {
/* WORKS */
font-size: calc(var(--block-font-size)*1.5);
}
// SCSS
.box {
$indent: 30px;
margin: $indent; /* 30px */
/* is ignored as changed
after value is applied */
$indent: 50px;
}
.box:hover{
/* is ignored as no
assignement is provided after this */
$indent: 80px;
/* margin: $indent; to apply */
}
// CSS
.box {
--indent: 30px;
margin: var(--indent); /* 50px */
/* is applied as native
variables are alive
as other CSS props */
--indent: 50px;
}
.box:hover{
/* is applied, so
margin: 80px; on hover */
--indent: 80px;
}
// SCSS
.text {
$text-size: 20px;
font-size: $text-size;
}
.active {
$text-size: 30px;
}
/* CSS */
.text {
--text-size: 20px;
font-size: var(--text-size);
}
.active {
--text-size: 30px;
}
<!-- HTML -->
<div class="text">.text</div>
<div class="text active">.text.active</div>
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;
}
/* CSS */
@supports (--css: variables){
/* supported */
}
@supports (not(--css: variables)){
/* not supported */
}
// JavaScript
const isSupported =
CSS.supports('--css', 'variables');
if (isSupported) {
/* supported */
} else {
/* not supported */
}
<!-- HTML (in case of older browsers support) -->
<link href="without-css-custom-properties.css"
rel="stylesheet" type="text/css" media="all" />
<script>
if(isSupported){
removeCss('without-css-custom-properties.css');
loadCss('css-custom-properties.css');
// + apply some application enhancements
// using the custom properties
}
</script>
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 map = document.querySelector('.example').styleMap;
console.log( map.get('font-size') );
// CSSUnitValue {value: 5, unit: "px", type: "length"}
// JS -> JS
console.log( new CSSUnitValue(5, "px") );
// CSSUnitValue {value: 5, unit: "px", type: "length"}
// JS -> CSS
// set style "transform: translate(50px, 80%);"
elem.styleMap.set('transform',
new CSSTransformValue([
new CSSTranslation(
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"
});
@property --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
- limited API -> very performant
- use the JS additions, essentially- ECMAScript 2015+ Classes with named methods
- 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 */
.multi-border {--border-top-width: 10; border-image: paint(border-colors);}
// 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) {
const elWidth = size.width;
const topWidth = styleMap.get('--border-top-width').toString();
// draw a border
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
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
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). 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.