CSS Houdini More than magic!

Vincent De Oliveira · @iamvdo

Hello, it’s @iamvdo

Screenshot of extensiblewebmanifesto.org

For CSS ?

Build CSS Polyfills

Today, it’s complicated impossible to polyfill CSS, even with JS.

https://philipwalton.com/articles/the-dark-side-of-polyfilling-css/

Build its own graphical effects

Build its own langage addons

in a nutshell

Innovate

Welcome Houdini!

Browser rendering*

JS HTML / CSS Parsing Parsing DOM DOM CSSOM CSSOM Typed OM Render Tree Render Tree Layout Layout Paint Paint Composite Composite Screen

* More or less (depending browser)

Many APIs

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API

Houdini is
JS-in-CSS

Or rather CSS-by-JS

Can I use Houdini?

Progressive enhancement FTW

Warning, fresh paint!

Evrything that is showed here can stop running anytime ¯\_(ツ)_/¯

CSS Typed OM

CSSOM++

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • API to manipulate CSS
  • The new CSSOM
  • No more concatenations
  • Better optimizations by the browser
  • Easier to parse
// CSSOM
el.style.width = '50px'
el.style.setProperty('width', '50px')
el.style.setProperty('transform', 'translate(' + x + 'px, ' + y + 'px)')
// Typed OM
el.attributeStyleMap.set('width', '50px')
el.attributeStyleMap.set('width', CSS.px(50))
el.attributeStyleMap.set('transform', new CSSTranslate(CSS.px(x), CSS.px(y)))

el.computedStyleMap().get('width') // CSSUnitValue {value: 50, unit: 'px'}

let [x, y] = [10, 10];
let transform = new CSSTranslate(CSS.px(x), CSS.px(y))
transform.x.value = 50
transform.toString() // "translate(50px, 10px)"
// Parse CSS
let css = CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

let css = CSSStyleValue.parse('transform', 'translate3d(10px,10px,0) scale(0.5)');
/*
CSSTranslateValue {
  0: CSSTranslate {
    is2D: false
    x: CSSUnitValue {value: 10, unit: 'px'}
    y: CSSUnitValue {value: 10, unit: 'px'}
    z: CSSUnitValue {value: 10, unit: 'px'}
  }
  1: CSSScale {
    is2D: true
    x: CSSUnitValue {value: 0.5, unit: 'number'}
    y: CSSUnitValue {value: 0.5, unit: 'number'}
    z: CSSUnitValue {value: 1, unit: 'number'}
  }
  is2D: false
}
*/

Typed OM == Houdini foundation!

CSS Custom Properties

Not specific to Houdini

.el {
  box-shadow: 0 3px 3px rgba(0,0,0,.75);
}
.el:hover {
  box-shadow: 0 15px 10px rgba(0,0,0,.75);
}
/* Extend CSS: custom properties */
.el {
  box-shadow: var(--box-shadow-x, 0) var(--box-shadow-y, 3px)
              var(--box-shadow-blur, 3px)
              var(--box-shadow-color, rgba(0,0,0,.75));
}
.el:hover {
  --box-shadow-y: 15px;
  --box-shadow-blur: 10px;
}
HELLO CSS!
.el {
  text-shadow: 
    var(--x, 0)
    var(--y, 3px)
    var(--blur, 3px)
    var(--color, rgba(0,0,0,.75));
}
// Change in JS
el.addEventListener('mousemove', e => {
  el.attributeStyleMap.set('--x', e.offsetX)
  el.attributeStyleMap.set('--y', e.offsetY)
  el.attributeStyleMap.set('--blur', blur)
})

CSS Custom Properties ++

Properties & Values API

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
// Set an animatable property
CSS.registerProperty({
  name: '--box-shadow-blur',
  syntax: '<length>',
  inherits: false,
  initialValue: '0px'
})
.el {
  transition-property: --box-shadow-blur, --box-shadow-y;
  transition-duration: .45s;
}
.el:hover {
  --box-shadow-y: 15px;
  --box-shadow-blur: 10px;
}

Creative Animate a gradient

Worklets

  • Isolated environment
  • Similar to Web Workers
  • Light, reusable, trashable
  • Only HTTPS

Paint API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Draw a canvas in background images
  • Automatically redrawn, resized
  • In its own thread
  • No need DOM elements
  • CSS.paintWorklet.addModule('paint.js')
  • Worklet: registerPaint()
  • CSS: paint()
CSS!
registerPaint('circle', class {
  paint(ctx, geom, props, args) {
    // Get the center point and radius
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);

    // Draw the circle
    ctx.fillStyle = 'deeppink';
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
  }
}
.el {
  background-image: paint(circle);
}
CSS!
registerPaint('circle-props', class {
  static get inputProperties() { return ['--circle-color']; }
  paint(ctx, geom, props, args) {
    // Determine the center point and radius.
    const x = geom.width / 2;
    const y = geom.height / 2;
    const radius = Math.min(x, y);

    // Draw the circle
    ctx.fillStyle = props.get('--circle-color').value;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.fill();
  }
}
.el {
  --circle-color: deepskyblue;
  background-image: paint(circle-props);
}
Click!
registerPaint('circle-ripple', class {
  static get inputProperties() { return [ '--circle-color',
    '--circle-radius', '--circle-x', '--circle-y'
  ]}
  paint(ctx, geom, props, args) {
    const x = props.get('--circle-x').value;
    const y = props.get('--circle-y').value;
    const radius = props.get('--circle-radius').value;
  }
}el.addEventListener('click', e => {
  el.classList.add('animating');
  el.attributeStyleMap.set('--circle-x', e.offsetX);
  el.attributeStyleMap.set('--circle-y', e.offsetY);
});
.el {
  --circle-radius: 0;
  --circle-color: deepskyblue;
  background-image: paint(circle-ripple);
}
.el.animating {
  transition: --circle-radius 1s,
              --circle-color 1s;
  --circle-radius: 300;
  --circle-color: transparent;
}

Creative Artistic drawing

Creative Backgrounds Corners gradient

Polyfill Backgrounds corner-shape

Polyfill Creative Backgrounds background-filter / opacity / rotate

Creative Backgrounds Highlighter marker

Creative Borders Tooltip arrow

Creative Borders Rough borders

Reusable and trashable worklet ?

Slightly bypassed using seed random numbers

Creative Masks Random bubbles

Creative Masks Irregular grid

Creative 🤯 JS-in-CSS

Layout API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Build its own layout
  • À la Flexbox / Grid
  • Only for the children
  • On its own thread*
  • Pretty complex
  • CSS.layoutWorklet.addModule('layout.js')
  • Worklet: registerLayout()
  • CSS: layout()
CSS!
registerLayout('center', class {
  *layout(children, edges, constraintSpace, props) {
    let childFragments = [];
    for(let child of children) {
      let childFragment = yield child.layoutNextFragment();
      let childHalfSize = childFragment.inlineSize / 2;
      let parentHalfSize = constraintSpace.fixedInlineSize / 2;
      childFragment.inlineOffset = parentHalfSize - childHalfSize;
      childFragments.push(childFragment);
    }
    return { childFragments };
  }
});
.parent {
  display: layout(center);
}
CSS!
registerLayout('position', class {
  static get inputProperties() { return ['--position-x']; }
  *layout(children, edges, constraintSpace, props) {
    let posx = props.get('--position-x').value;
    let pos = constraintSpace.fixedInlineSize / (100 / posx);
  }
});
.parent {
  display: layout(position);
  --position-x: 50%;
}
Click outside!
registerLayout('position', class {
  static get inputProperties() { }
  *layout(children, edges, constraintSpace, props) { }
});parent.addEventListener('click', e => {
  parent.classList.add('animating');
  let pos = 100 * e.offsetX / parentWidth;
  parent.attributeStyleMap.set('--position-x', CSS.percent(pos));
});
.parent {
  display: layout(position);
  --position-x: 50%;
}
.parent.animating {
  transition: --position-x 1s;
}

Creative Masonry

Polyfill Android RelativeLayout

Creative SVG Path

Animation API

Worklet

CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API
  • Animations on their own thread
  • Based on Web Animations API
  • Timeline based on time or scroll
  • Only for performance
  • Only JavaScript
  • CSS.animationWorklet.addModule('anim.js')
  • registerAnimator()
  • new WorkletAnimation()

Based on a Scott Kellum idea

registerAnimator('simple', class {
  animate(currentTime, effect) {
    effect.localTime = currentTime;
  }
});.cube {
  --angle: 0;
  transform: rotateX(var(--angle)) rotateZ(45deg) rotateY(-45deg);
}
new WorkletAnimation('simple',
  new KeyframeEffect(el, [
      { '--angle': 0 },
      { '--angle': '1turn' }
    ],
    { duration: 1 }
  ),
  new ScrollTimeline({
    scrollSource: scrollElement,
    timeRange: 1
  }),
).play();
Scroll!
Scroll!
Scroll!
registerAnimator('parallax', class {
  constructor(options = {}) {
    this.factor = options.factor || 1;
  }
  animate(currentTime, effect) {
    effect.localTime = currentTime * this.factor;
  }
});
for (let i = 0; i < els.length; i++) {
  new WorkletAnimation('parallax',
    new KeyframeEffect(els[i], [
        { transform: new CSSTranslate(0, 0) },
        { transform: new CSSTranslate(0, CSS.px(scrollHeight)) }
      ], { duration: 1 }
    ),
    new ScrollTimeline({
      scrollSource: scrollElement,
      timeRange: 1
    }),
    { factor: (i / 2) * (1.2 - 1) + 1 }
  ).play();
}

CSS Parser, Render Tree, Font Metrics

The future?
CSS Parser API Box Tree API (render tree) CSS Typed OM CSS Properties & Values Worklets CSS Paint API CSS Layout API Animation Worklet API Font Metrics API

Sky’s the limit

Wait & See

Questions

New graphical effects?

  • Increase creativity
  • Everything is JavaScript
  • Pretty complex to use

Performance?

  • Perf boost, mainly for animations
  • Not always true (today)

Standardization?

  • No need to wait for standards
  • Everything have to be build from the ground (prediction: heterogeneous ecosytem)
  • Standards can evoluate

Polyfill?

  • More performant, more easily
  • Quickly limited (today?)
  • Many impossible effects: element(), backdrop-filter, CSS Shaders 😍, etc.

Browser engine innovation?

  • Performance optimizations by the browser
  • Houdini based on an actual rendering pipeline
  • Firefox and its new engine WebRender?

Security?

  • ex: paint() not on links
  • Only HTTPS
  • Similar fate than CSS Shaders?

Thanks!

https://slides.iamvdo.me/waq19

https://css-houdini.rocks

Vincent De Oliveira · @iamvdo · iamvdo.me

@iamvdo Version FR