From CSS Custom Properties to JavaScript Worklets and back

About me

CSS Custom Properties


Variables and Operators (+, -, *, /, %)

$font-size: 10px;
$font-family: Helvetica, sans-serif;

body {
  font: $font-size $font-family;

  font-size: 1.5 * $font-size;

Preprocessor variable problems

  • Each preprocessor has own syntax
  • Additional setup
  • Recompilation after changes is required
  • Compilation takes time
  • Absence of interaction with the browser
  • Not aware of the DOM structure

Native CSS Syntax

/* declaration */
--VAR_NAME: <declaration-value>;
/* usage */
/* 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

Declaration and usage

Supported in all the major browsers

Valid examples

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

Declaration and use cases

/* Default values */
  /*--- 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-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);

Declaration and use cases

CSS-Wide Keywords and "all" Property

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

  --rotate: rotate(90deg);
.transform {
  transform: scale(2) rotate(10deg);

  transform: scale(2) rotate(90deg);

Emulating non-existing CSS rule

Operations: +, -, *, /

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

All together: calc() and change values separately

Calculate all the app colors from the base-color (theming)

Changes to Custom Props have immediate effect

.box {
  $indent: 30px;

  margin: $indent; /* 30px */

/* is ignored as changed
   after value is applied */
  $indent: 50px;

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

    /* is applied, so
   margin: 80px; on hover */
    --indent: 80px;

CSS Properties are aware ​of the DOM structure

.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"></div>

Using Custom Properties With JavaScript

  • pass breakpoints data from CSS
  • read values...
const breakpointsData = 

// GET
const phone = getComputedStyle(breakpointsData)

// SET
    .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

Check if supported

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

  // + apply some application enhancements
  // using the custom properties

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
<length> | <percentage> 5%
border-width [ <length> | thick | medium | thin ]{1,4} 2px medium 4px

Houdini group

& Values








& Values


CSS Typed OM spec

// CSS -> JS
const map = document.querySelector('.example').styleMap;
console.log( map.get('font-size') );
// CSSSimpleLength {value: 12, type: "px", cssText: "12px"}

// JS -> JS
console.log( new CSSUnitValue(5, "px") );
// CSSUnitValue{value:5,unit:"px",type:"length",cssText:"5px"}

// JS -> CSS
// set style "transform: translate3d(0px, -72.0588%, 0px);"
    new CSSTransformValue([
        new CSSTranslation(
          0, new CSSSimpleLength(100 - currentPercent, '%'), 0

behind the “Experimental Web Platform features” flag in

  name: "--stop-color",
  syntax: "<color>",
  inherits: false,
  initialValue: "black"

CSS Properties and Values API 2

@property --stop-color {
  syntax: "<color>",
  inherits: false,
  initialValue: black

Why it's important



"syntax" of CSS properties

Default: "*"

Supported Values:


"<length> | <percentage>"

both, but not calc() combinations



both + calc() combinations of both types


"big | bigger"

accepts either value



accepts a list of length values


CSS Custom Properties

should be animatable

since they are provided with types?

Let's try!

Native CSS Animation

Web animation API

  {cssProperty: 'fromValue'},
  {cssProperty: 'toValue'}
], {
    duration: timeInMs,
    fill: 'none|forwards|backwards|both',
    delay: delayInMs,
    easing: 'linear|easy-in|cubic-bezier()...',
    iterations: iterationCount|Infinity
    { 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;

Native JavaScript animation





CSS Custom Properties

CSS Property Types

Typed OM API



Browser rendering

image credits: 1, 2



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

- limited API -> very performant

- use the JS additions, essentially- ECMAScript 2015+ Classes with named methods

- triggered when needed and possible

Paint Worklet

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:

Paint Worklet example

/* CSS */
.multi-border {--border-top-width: 10; border-image: paint(border-colors);}

// JS
    name: '--border-top-width',
    syntax: '<number>',
    inherits: false,
    initialValue: '0',
// add a Worklet

// 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').value;
        // draw a border
        ctx.fillStyle = 'magenta';
        ctx.moveTo(0, 0);
        ctx.lineTo(elWidth, 0); ctx.lineTo(elWidth, topWidth);
        ctx.lineTo(0, topWidth); ctx.lineTo(0, 0);

Browsers are smart and trigger paint only when and where needed (demo)

Activate "Rendering pane" -> "Paint Flashing" to highlight paints

Layout Worklet

- to create own layout algorithms
- having access to set constraints, behaviour, boundaries

- interact blocks, fragments and even text

/* CSS */
.center {
  display: layout(centering);


/* CSS */
.photos {
  display: layout(masonry);
// JS (layout worklet)
  class extends Layout {
    *layout(/*...*/) {/*...*/}

Animation Worklet

Sticky Header implemented in the stable version of Chrome using Houdini task force achievements

In-sync with the compositor thread on a “best effort” basis
(tried on every frame, but may be as requestAnimationFrame)

- animations and mostly scroll effects:

- sticky elements

- smooth scroll animations

- scroll up bar

Use cases

Polyfill is available 🎉

Compositor only properties




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

Promote with "will-change"



Animation Worklet- CSS/JS parts

/* CSS */
.scroll-position {
    /* shifted to the left */
    left: -100%;
    /* CSS animator directive
    to link up elements to an animator instance */
    --animator: scroll-position-worklet scrollerElementReference;

/* JS (add a Worklet module) */

Animation Worklet itself

/* Animators are classes that are run in the worklet
 and get to control certain attributes of DOM elements. */
registerAnimator('scroll-position-worklet', class ScrollPositionAnimator {
 static get elements() {
  return [{
    // linked element name
    name: 'scrollerElementReference',

    // properties the animator needs to read to compute the animation
    // animator can be skipped if input props not changed since last frame
    inputProperties: [],

    // Output properties are properties that the animator might mutate
    // "opacity" and/or "transform"
    outputProperties: ['transform']
 static get timelines() {
  // timeline options list
 animate(elementMap, timelines) {
  // Animation frame logic goes here

Timeline and animate

registerAnimator('scroll-position-worklet', class {
 // listen global vertical scroll
 static get timelines() {
   return [{type: 'scroll', options: {orientation: 'vertical'}}]
 animate(elementMap, timelines) {
  // current scroll position in range [0-100%] (of page)
  const scrollPosition = parseFloat(timelines[0].currentTime) * 100;
  elementMap.get('scrollerElementReference').forEach(elem => {
   // set CSS "transform:translate3d(`${scrollPosition}%`, 0, 0)";
   elem.outputStyleMap.set('transform', new CSSTransformValue([
    new CSSTranslation(
     new CSSSimpleLength(scrollPosition, '%'),0,0

Animation Worklet result

"endScrollOffset" option

static get timelines() {
 return [{type: 'scroll', options: {
  orientation: 'vertical',
  // set scroll position to 100% at 375px
  endScrollOffset: '375px'


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

Thank you!

From CSS Custom Properties to JavaScript Worklets and back

By Serg Hospodarets

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). All this brings Front-End development to the next level, parts of which are already available for the developers.

  • 1,252
Loading comments...

More from Serg Hospodarets