CSS Houdini:
From CSS Custom Properties to JavaScript Worklets and back

About me

CSS Custom Properties

Native CSS Syntax

/* 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

Declaration and usage

Supported in all the major browsers

Usage example: Emulating non-existing CSS rule

Using Custom Properties With JavaScript

  • pass breakpoints data from CSS
  • read values...
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;
}
  • assign CSS value calculated in JS
  • update UI depending on the application state...

Rotate page elements using CSS variables

Current constraints

Custom CSS Properties  by default are:

  • not typed
  • in result - not animatable

Why animation is important

Why typed CSS properties and values are important

  • support and validation in IDE
  • syntax highlight
  • performance
  • Browser support
  • DevTools support
  • linters, compilers...

Introducing CSS property types?

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

Houdini group

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"
});

Why it's important

Without

With

"syntax" of CSS properties

Default: "*"

Supported Values:
"<length>"
"<number>"
"<percentage>"
"<length-percentage>"
"<color>"
"<image>"
"<url>"
"<integer>"
"<angle>"
"<time>"
"<resolution>"
"<transform-function>"

Examples:

"<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

And...

CSS Custom Properties

should be animatable

since they are provided with types?

Let's try!

Native CSS Animation

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 🎉

Native JavaScript animation

CSS/JS/Browser

CSS

JavaScript

Browser

CSS Custom Properties

CSS Property Types

Typed OM API

CSSOM

?

Browser rendering

image credits: 1, 2

Pixel

rendering
pipeline

Internal browser engine

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

Paint Worklet example

/* 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

Use cases

Worklet and Web Animation API polyfills are available 🎉

CSS
Animation
Worklet
API

Compositor

Accelerated (e.g. compositor only) properties

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"

opacity

transform

Scroll Position Effects Demo- CSS/HTML/JS

<!-- 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);

Animation Worklet itself

/* 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

/* 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 Worklet result

"endScrollOffset" and "timeRange" options

// animation options
{
 pageTimeline: new ScrollTimeline({
  scrollSource,
  orientation: 'vertical',
  endScrollOffset: '375px',
  timeRange: 375
}

Consclusions

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.)

Thank you!

CSS Houdini-short: From CSS Custom Properties to JavaScript Worklets and back

By Serg Hospodarets

CSS Houdini-short: From CSS Custom Properties to JavaScript Worklets and back

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.

  • 145
Loading comments...

More from Serg Hospodarets