Design System Magic with Houdini

Houdini Bunny

I'm @snugug

This is the Houdini bunny

These slides are online!
https://talks.page.link/houdini-design-systems

What is Houdini?!

The objective of the CSS-TAG Houdini Task Force (CSS Houdini) is to jointly develop features that explain the “magic” of Styling and Layout on the web.

Practically, though, what does that mean?

Extending CSS via JS

so authors no longer have to wait a decade for standards bodies and browsers to do something new

But Wait! Can't We Do This Already!

Not Quite

  • It's currently not possible to extend CSS with JS, only write JS that mimics CSS
  • Actually polyfilling CSS, or introducing new features (like CSS Grids), is hard-to-impossible to do. Doubly so in a way that's not terrible for performance.
  • Houdini will let authors tap in to the actual CSS engine, finally allowing them to extend CSS, and do so at CSS Engine speeds

Much like Service Workers are a low-level JavaScript API for the browser's cache Houdini introduces low-level JavaScript APIs for the browser's render engine

Nope

Kinda

WARNING Heads Up!

Some of Houdini is pretty stable and getting cross-browser implementations, some is still quite a bit away from being usable, and the landscape is currently in flux with Edge moving to Chromium. Most of what we're going to talk about today are the mostly stable bits, but you're going to need to use polyfills and get your progressive enhancement on in order to use this stuff today.

Core Magical Secrets

Worklets

Worklets are extension points for rendering engines

They're like Web Workers, but with a much smaller scope, can be parallelized, live on multiple threads, and most importantly, get called by the render engine, not us

// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// From inside the browser's context
// Script gets loaded in the main thread, then sources are sent to Worklet threads
// Same script can be loaded in to multiple Worklet threads
window.demoWorklet.addModule('path/to/script.js');
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// worklet.addModule returns a Promise!
// Sometimes post-register work is done with loaded worklets, this makes it possible
Promise.all([
  window.demoWorklet1.addModule('path/to/script1.js'),
  window.demoWorklet2.addModule('path/to/script2.js'),
]).then(worklets => {
  // Worklets have loaded and can be worked on!
});
// From https://drafts.css-houdini.org/worklets/ September 1, 2017 Editor's Draft

// The kind of worklet it is
registerDemoWorklet('name', class { // The name we'll call this worklet

  // Each worklet can define different functions to be used
  // These will be called by the render engine as needed
  process(arg) {
    // Stuff happens in here! What happens depends on the worklet
    // Sometimes it'll return something
    // Other times it'll work directly on the arguments
    return !arg;
  }
});
Artboard 1 worklet.addModule process,

Worklets are the underlying foundation to which all of Houdini is based. They're the magic that makes it happen. They're what make you powerful.

Typed OM

Typed OM exposes structure, beyond simple strings, for CSS Values. These then can be manipulated and retrieved in a more performant manner, and are part of the new CSSStyleValue class.

  • CSSKeywordValue - CSS Keywords and other identifiers (like inherit)
  • CSSPositionValue - Position (x and y) values
  • CSSTransformValue - A list of CSS transforms consisting of CSSTransformComponent including CSSTranslation, CSSRotation, CSSRotation, CSSScale, and CSSSkew
  • CSSUnitValue - Numeric values that can be expressed as a single unit (or a naked number or percentage)
  • CSSMathValue - and its subclasses CSSMathSum, CSSMathProduct, CSSMathMin, CSSMathMax, CSSMathNegate, and CSSMathInvert. Complicated numeric values (like calc, min, max, etc…)
.example {
  background-position: center bottom 10px;
}
// From https://drafts.css-houdini.org/css-typed-om/ March 7, 2018 Editor Draft

let map = document.querySelector('.example').computedStyleMap();

map.get('background-position').x;
// CSSUnitValue { value: 50, unit: 'percent' }

map.get('background-position').y;
// CSSMathSum {
//   operator: 'sum',
//   values: [ // CSSNumericArray
//     { value: -10, unit: 'px' }, // CSSUnitValue
//     { value: 100, unit: 'percent' }, // CSSUnitValue
//   ]
// }

The Typed OM gives us the structure our CSS needs to cast the spells we want.

Dr Strange

Yah, But What Can I DO With This?

Rad

The Rad
Custom Stuff

Please allow me to introduce you to ...

window.CSS

Custom Properties

Current Situation

.thing {
  --my-color: green;
  --my-color: url('not-a-color'); // It's just a variable! It doesn't know any better
  color: var(--my-color);
}

But Then

window.CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>', // Now it's def a color. That `url` junk is skipped!
});
Structure of a Registered Property
// From https://drafts.css-houdini.org/css-properties-values-api/ July 19, 2017 Editor's Draft

window.CSS.registerProperty({
  name: '--foo', // String, name of the custom property
  syntax: '<color>', // String, how to parse this property. Defaults to *
  inherits: false, // Boolean, if true should inherit down the DOM tree
  initialValue: 'black', // String, initial value of this property
})

The following are valid types for syntax

  • <length> - Any valid length value
  • <number> - Number values
  • <percentage> - Any valid percentage
  • <length-percentage> - Any valid length or percentage value, any valid calc() expression combining length and percentage
  • <color> - Any valid color value
  • <image> - Any valid image value
  • <url> - Any valid url value
  • <integer> - Any valid integer value
  • <angle> - Any valid angle value
  • <time> - Any valid time value
  • <resolution> - Any valid resolution value
  • <transform-list> - A list of valid transform-function values
  • <custom-ident> - Any valid custom-ident value

syntax also allows for combiners

  • <length> - A single length value
  • <image> | <url> - Accepts a single image or a single URL
  • big | bigger | BIGGER - Accepts the ident "big", the ident "bigger", or the ident "BIGGER"
  • <length>+ - Accepts a space-separated list of length values
  • <color># - Accepts a comma-separated list of color values

Custom Properties

In Your Design System

Register in CSS


Proposed CSS Syntax

@property --my-color {
  syntax: '<color>';
}

JS Equivalent

if ('registerProperty' in CSS) {
  window.CSS.registerProperty({
    name: '--my-color',
    syntax: '<color>',
  });
}

Make CSS Variables Smarter


:root {
  --main-blue: #466BB0;

  // When this lands, we can drop the above!
  @property --my-blue {
    syntax: '<color>';
    inherits: true;
    initial-value: #466BB0;
  }

  // Make sure that --background is always valid!
  @property --background {
    syntax: '<image> | <color>';
    inherits: false;
  }
}

Make Real Custom Properties


:root {
  // For use later with the Paint API
  @property --theme-color {
    syntax: 'blue|green|red|yellow|grey';
    inherits: true;
    initial-value: blue;
  }

  // For use later with the Layout API
  @property --columns {
    syntax: '<integer> | auto';
    inherits: false;
    initial-value: 2;
  }

  @property --padding {
    syntax: '<integer>';
    inherits: true;
    initial-value: 0;
  }
}

Paint API

  • Ever wanted to use canvas* as a background, a mask, or a border in CSS?
  • With the styling flexibility of an element?
  • And the scalability of SVG?

Of Course Not!
But with the Paint API you can, and it's really cool.

*2D drawing without access to window

paintWorklet Class
// From https://drafts.css-houdini.org/css-paint-api/ January 28, 2018 Editor's Draft

class myPaint {
  // Input properties from element to look for
  static get inputProperties() { return ['--foo']; }
  // Input arguments that can be passed to `paint` function
  static get inputArguments() { return ['<color>']; }
  // Alpha allowed?
  static get alpha() { return true; }

  paint(ctx, size, props, args) {
    // ctx - drawing context
    // size - size of the box being painted
    // props - inputProperties
    // args - array of passed-in arguments

    // Paint code goes here.
  }
}

Define - js/circle.js

registerPaint('circle', class {
    static get inputProperties() { return ['--circle-color']; }
    paint(ctx, size, props) {
      // Change the fill color.
      const circle = props.get('--circle-color'); // This is a CSSStyleValue!

      // Determine the center point and radius.
      const xCircle = size.width / 2;
      const yCircle = size.height / 2;
      const radiusCircle = Math.min(xCircle, yCircle) - 2.5;

      // Draw the circle \o/
      ctx.beginPath();
      ctx.arc(xCircle, yCircle, radiusCircle, 0, 2 * Math.PI);
      ctx.fillStyle = circle;
      ctx.fill();
    }
});

Import

CSS.paintWorklet.addModule('js/circle.js');

Paint API


In Your Design System

Paint API Polyfill Wizard
Web Components logo

A Rounded Tab Component

<rounded-tab>First</rounded-tab>
<rounded-tab>Second</rounded-tab>
<rounded-tab>Third</rounded-tab>
One Two Three

The first section! Isn't this cool?

The second section! Isn't this cool?

The third section! Isn't this cool?

<template id="tab-template">
  <style>
    .tab {
      --tab-border-radius: calc(5px * var(--tab-multiplier));
      --tab-border-offset: calc(30px * var(--tab-multiplier));
      background: red;
      border-image-outset: var(--tab-border-offset);
      border-image-slice: 0 fill;
      border-image-source: paint(TabBottom);
      border-radius: var(--tab-border-radius) var(--tab-border-radius) 0 0;
      display: inline-block;
      font-size: 1em;
      padding: 0.25em 0.5em;
      box-sizing: border-box;
    }
  </style>
  <div class="tab" part="body"><slot></slot></div>
</template>
CSS.registerProperty({
    name: '--tab-multiplier',
    syntax: '<number>',
    inherits: true,
    initialValue: 1
  });

  CSS.registerProperty({
    name: '--tab-position',
    syntax: 'left|right|middle',
    inherits: false,
    initialValue: 'middle'
  });
}

CSS.paintWorklet.addModule('./tabs.js');
class RoundedTab extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('tab-template');
    const content = template.content;
    this.attachShadow({ mode: 'open' })
      .appendChild(content.cloneNode(true));
  }
}

customElements.define('rounded-tab', RoundedTab);

Paint-Powered Web Component Tabs

rounded-tab::part(body), .tabs--section { --tab-multiplier: 1; background: red; color: white; } rounded-tab:first-of-type::part(body), .tabs--section:first-of-type { --tab-position: left; background: blue; } rounded-tab:last-of-type::part(body), .tabs--section:last-of-type { --tab-position: right; background: green; }
One Two Three

The first section! Isn't this cool?

The second section! Isn't this cool?

The third section! Isn't this cool?

LAYOUT API

  • Literally, make your own display properties
  • Polyfill that awesome new layout spec you love!
  • Everyone likes a good Masonry layout, add one without a performance hit!

this spec is crazy complicated and I don't quite understand the whole thing yet

layout
layoutWorklet Class
// From https://drafts.css-houdini.org/css-layout-api/ February 20, 2019 Editor's Draft

class myLayout {
  // Properties to look for on calling element
  static inputProperties = ['--foo'];
  // Properties to look for on direct child elements
  static childrenInputProperties = ['--bar'];
  // Options for the Layout
  static layoutOptions = { childDisplay: 'normal', sizing: 'block-like' };

  // Determines how a box fits its content or fits in to our layout context
  async intrinsicSizes(children, edges, styleMap) {
    // children - Child elements of box being laid out
    // edges - `LayoutEdges` of Parent Layout
    // styleMap - Typed OM style map of box being laid out
  }

  async layout(children, edges, constraints, styleMap, breakToken) {
    // children - Child elements of Parent Layout
    // edges - `LayoutEdges` of Parent Layout
    // constraints - `Layout Constraints` of Parent Layout
    // styleMap - Typed OM style map of Parent Layout
    // breakToken - Token (if paginating for printing for example) to resume layout at
  }
}
// From https://drafts.css-houdini.org/css-layout-api/#example-13a91ee5 February 20, 2019 Editor's Draft

registerLayout('centered', class {
  async intrinsicSizes(children, edges, styleMap)
    // Get all the sizes!
    const childrenSizes = await Promise.all(children.map((child) => {
      return child.intrinsicSizes();
    }));

    // How large the box can be given unlimited space
    //  in order to fit its content with minimum unused
    //  space
    const maxContentSize = childrenSizes.reduce((max, childSizes) => {
      return Math.max(max, childSizes.maxContentSize);
    }, 0) + edges.inline;

    // How small the box can be so that its content
    //  doesn't overflow
    const minContentSize = childrenSizes.reduce((max, childSizes) => {
      return Math.max(max, childSizes.minContentSize);
    }, 0) + edges.inline;

    return { maxContentSize, minContentSize };
  }

  // ...
  // ...
  async layout(children, edges, constraints, styleMap) {
    // Determine our (inner) available size
    const availableInlineSize = constraints.fixedInlineSize - edges.inline;
    const availableBlockSize = constraints.fixedBlockSize ?
      constraints.fixedBlockSize - edges.block : null;

    const childFragments = [];
    const childConstraints = { availableInlineSize, availableBlockSize };

    // Build fragments inside the content area
    const childFragments = await Promise.all(children.map((child) => {
      return child.layoutNextFragment(childConstraints);
    }));

    // ...
    // ...
    // Start counting block positioning from the start of block edges
    let blockOffset = edges.blockStart;
    for (let fragment of childFragments) {
      // Set fragment's block offset
      fragment.blockOffset = blockOffset;
      // Center the block inline
      fragment.inlineOffset = Math.max(
        edges.inlineStart,
        (availableInlineSize - fragment.inlineSize) / 2);

      // Add the fragment's block size to the offset to set the next below this one
      blockOffset += fragment.blockSize + styleMap.get('--gap').value;
    }

    // Add edges back on at the end
    const autoBlockSize = blockOffset + edges.blockEnd;

    // Return the element's generated blockSize, and child fragments
    return {
      autoBlockSize,
      childFragments,
    };
  }
});

LAYOUT API

In Your Design System

Animated Circular Navigation Menu

.circle { --item-size: 32; --padding: 32; --angle: 0; display: layout(circle); transition-property: --angle, --padding, --offset, fill; } .circle[data-state="active"] { --padding: 32; --angle: 0; }
Houdini Bunny

https://talks.page.link/houdini-design-systems