CSS - tutorial - 28 - houdini

revision:


Content

definition and usage how does Houdini handle web page rendering issues? browser support basic example worklet Typed Object Model API custom properties and values API font metrics API paint API layout API animation API parser worklet


definition and usage

top

CSS Houdini is a set of APIs that expose parts of the CSS engine, giving developers the power to create extensions for CSS. These extensions might be to polyfill features that are not yet available in a browser, experiment with new ways of doing layout, or add creative borders or other effects.

Polyfill is a piece of JavaScript code that provides modern features in the older browser that lack support. It acts as a browser fallback for missing properties in older browsers. For example, a polyfill code can provide the CSS "outline property" in Internet Explorer 7, where the property is not supported. But, polyfill has a lot of drawbacks. The main downside of using a CSS polyfill is that it negatively impacts the browser's performance. The polyfill code is never as efficient as the native CSS code. Hence it can never work as a replacement.

While many Houdini examples showcase the creative possibilities of the APIs, there are many practical use cases. For example, you can use Houdini to create advanced custom properties with type checking and default values.

The ultimate goal of CSS Houdini is to give developers more control over the browser's rendering process and improve CSS capabilities without compromising performance.


how does Houdini handle web page rendering issues?

top

Houdini introduces a new set of APIs that let the developers access the parts of the browser rendering pipeline, such as access to the layout, painting, and other processes of the website. Developers can go above the regular CSS styling and create their properties, layout, and animations.

CSS Houdini consists of two sets of APIs: low-level APIs and high-level APIs. These APIs make it easier to create extensions for CSS (e.g. to polyfill features, experiment with new layout methods, add creative borders).

low-level APIs: worklets, Typed Object Model API, custom properties API, font metrics API. These APIs form the foundation of the high-level APIs.

high-level APIs: paint API, layout API, animation worklet API, parser API. These APIs represent the four stages of the browser's rendering pipeline.


browser support

top

as per November 2022


basic example

top

A regular CSS custom property consists of a "property name" and a "value". Therefore one might create a custom property called "--background-color" and expect it to be given a color value. The value is then used in the CSS as if it were the color value.

CSS codes:
            <div class="box"></div>          
            <style>
                :root {--background-color: blue; --border-color: burlywood;}
                .box {margin-left:15vw; width:15vw; height: 10vw; background-color: 
                    var(--background-color); border: 0.3vw solid var(--border-color)}
            </style>
        

In the above example however, there is nothing to stop someone using some other value for this property, perhaps setting it to a length. Having done so, anywhere that the property is used would have no background color as "background-color: 12px" is not valid. When browsers come across CSS they don't recognize as valid they throw that line away.

Using @property however, we can declare the custom property with a syntax of <color>. This shows that we need this property to have a value which is a valid color.


worklet

top

A key feature of Houdini is the worklet, which is a module, written in JavaScript, that extends CSS using one of the Houdini APIs.

A worklet is a lightweight version of a web worker and acts as an extension point of the browser rendering engine for the developers. Each worklet runs in a separate context and has no access to the window, document objects, or high-level functions. Worklets run independently of the main thread and can be invoked at any point of the rendering pipeline.

Worklets are the JavaScript modules that attach themselves to the browser and make the high-level APIs work. They are imported and registered using a single line of JavaScript. Once a worklet has been registered it can be used in CSS just like any other value. This means that even if you are not a JavaScript developer, you can access Houdini APIs by using worklets other people have created.

Currently, CSS Houdini supports three worklets: paint worklet, layout worklet, animation worklet.

Importing a paint worklet: syntax: CSS.paintWorklet.addModule("url-of-worklet");

Using an imported worklet: syntax: body{background-color:paint(confetti)};


Typed Object Model API

top

Typed Object Model API adds more semantics to the CSS by exposing them as JavaScript objects.

Instead of taking a CSS value and parsing it into a "String", typed OM APIs provide you with a "JavaScript Object" with value and unit property. This approach is faster, and the code is more maintainable and less prone to errors. Using computedStyleMap() we get an object, which consists of the value and the unit. It is easier to perform the calculations and work with a numerical value.

Typed object model API performs type checking. So if you pass a wrong data type, it will show an error in the console.

example
            // Set and get an inline style
            element.attributeStyleMap.set('width', CSS.rem(48))
            element.attributeStyleMap.get('width')
            // => {value: 48, unit: "rem"}

            // Set and get computed style (note the "()" after computedStyleMap)
            element.computedStyleMap().set('height', CSS.em(12))
            element.computedStyleMap().get('height')
            // => {value: 12, unit: "em"}
        

custom properties and values API

top

Custom properties and values API allows to create CSS properties with data types, initial values, and inheritance rules. These properties can be registered using the "registerProperty()" method or the "@property" rule.

The CSS Custom properties API extends the CSS variables and adds Type checking. You can add a variable in the ':root{}' block and use it throughout your CSS file.

You can create your custom CSS property in two ways:

Using the registerProperty() method in your JavaScript file.


            CSS.registerProperty({
                name: "name-of-property",
                syntax: "",
                inherits: false / true,
                initialValue: "initial-value",
            });
          

Using @property in your CSS file.


            @property --property-name {
                syntax: "";
                inherits: false/true;
                initial-value: initial-value;
            
example:
            CSS.registerProperty({
                name: '--brandingColor',
                syntax: '',
                inherits: false,
                initialValue: 'goldenrod',
            })
        
example: in CSS file
            @property --brandingColor {
                syntax: '';
                inherits: false;
                initial-value: goldenrod;
            }
        

The syntax property represents the "type of the value". It accepts: <number>, <percentage>, <length-percentage>, <olor>, <image>, <url>, <integer> and <angle>. Setting the syntax helps the browser knowing how to transition between values. In CSS, you can transition between colors but cannot between gradients. Here, by defining "--brandingColor" we can, for example, animate a gradient!

example: gradient
code:
                <div class="spec element">
                    <div class="unit"></div>
                </div>
                <style> 
                    .element{ font-family: sans-serif;width: 40vw; height: 30vh; display: grid; 
                        place-items: center; background-color: #f5b4a5; }
                    .unit{--brandingColor: lightgreen; width: 20vw; height: 20vh; background: 
                        linear-gradient(90deg, green 0%, var(--brandingColor) 100%);
                    transition: --brandingColor 800ms ease-in-out;  border-radius: 4vw;}
                    .unit:hover{--brandingColor: red;}
                </style>
                <script>
                    CSS.registerProperty({ 
                        name: "--brandingColor",
                        syntax: "<color>", 
                        inherits: false,
                        initialValue: "#5f64e2",
                        });
                </script>
            

font metrics API

top

Font metrics API will contain methods that will allow to measure the dimensions of the textual elements that are being rendered on the screen. Using this API, text truncation on multiple line text elements can be performed.

This API is still in a very early stage of development, so there is a high chance that the specifications and features may change.


paint API

top

CSS paint API allows to generate graphics using JavaScript functions during the Paint stage of the browser's rendering pipeline. This is achieved with the help of Paint Worklet. In the background, Paint API uses the CanvasRenderingContext2D, which is the subset of HTML5 Canvas API.

Using a paint worklet, you can create custom backgrounds, borders, outlines, and more! You can define your own custom painting functions or use the existing worklets created by other developers.

Steps for defining the paint functions and for using them as CSS properties in a CSS file:

1/ Create your own worklet using the registerPaint() method or link an already created worklet using CDN.

example:
        registerPaint("name-of-worklet", {
           // .. code
        });
    

2/ Register and add it to your project using the addModule() method.

example
        CSS.paintWorklet.addModule("path-to-worklet.js-file");
    

3/ Use the paint() function as a CSS value to the background property.

example
        .demo {
            background: paint(name-of-worklet);
        }
    

4/ Add a polyfill script to ensure the worklet runs on all old browsers.

example
        	<script src="cdn-link-to-polyfill-file.js"></script>
    
example: paint API
test

Sorry 😥
Your browser does not support the Paint API
Try with chrome!

code:
                <div class="frame">
                    <div class="element2">test
                        <p class="announce">Sorry 😥<br>Your browser does not support the
                        Paint API<br>Try with chrome!</p>
                    </div>
                    <script>
                            CSS.registerProperty({
                            name: "--cornerbox-length",
                            syntax: "<length>",
                            inherits: false,
                            initialValue: "12vw",
                            });
                            CSS.registerProperty({
                            name: "--cornerbox-width",
                            syntax: "<length>",
                            inherits: false,
                            initialValue: "1.6vw",
                            });
                            CSS.paintWorklet.addModule('https://codepen.io/tomquinonero/pen/eYRXbRL.js');
                        </script>
                </div>
                <style>
                    .frame{margin-left: 4vw; font-family: sans-serif; height: 40vh; display: grid; 
                        place-items: center; background: #f5b4a5;}
                    .element2 {background: #5f64e2; color: #f5b4a5; font-size: 2.2vw; text-align: center;
                        padding: 2.4vw; width: 12vw; height: 12vw;
                    --cornerbox-color: #5f64e2; --cornerbox-length: 10vw; --cornerbox-width: 0.5vw; 
                    background: paint(cornerbox); transition: --cornerbox-length 800ms ease-in-out,
                    --cornerbox-width 800ms ease-in-out;}
                    .element2:hover { --cornerbox-length: 16vw; --cornerbox-width: 0.8vw;}
                    @supports (background: paint(cornerbox)) {
                        .announce {display: none;}
                    }
                </style>
                <script>
                    class cornerBox {
                        static get inputProperties() {
                            return [`--cornerbox-width`, `--cornerbox-length`, `--cornerbox-color`];
                        }
                        paint(ctx, size, props) {
                            const lineBox = parseInt(props.get(`--cornerbox-length`));
                            const lineBoxwidth = parseInt(props.get(`--cornerbox-width`));
                            const colorBox = props.get(`--cornerbox-color`).toString().trim();
                            if (lineBox != 0) {
                                ctx.lineWidth = lineBox;
                                ctx.beginPath();
        
                                /* UP Left */
                                ctx.moveTo(0, 0);
                                ctx.lineTo(0, lineBoxwidth);
                                ctx.moveTo(0, 0);
                                ctx.lineTo(lineBoxwidth, 0);
        
                                /* Up Right */
                                ctx.moveTo(size.width - lineBoxwidth, 0);
                                ctx.lineTo(size.width, 0);
                                ctx.moveTo(size.width, 0);
                                ctx.lineTo(size.width, lineBoxwidth);
        
                                /* Down Left */
                                ctx.moveTo(0, size.height - lineBoxwidth);
                                ctx.lineTo(0, size.height);
                                ctx.moveTo(0, size.height);
                                ctx.lineTo(lineBoxwidth, size.height);
        
                                /* Down Right */
                                ctx.moveTo(size.width, size.height - lineBoxwidth);
                                ctx.lineTo(size.width, size.height);
                                ctx.moveTo(size.width, size.height);
                                ctx.lineTo(size.width - lineBoxwidth, size.height);
        
                                ctx.strokeStyle = colorBox;
                                ctx.stroke();
                                }
                            }
                        }
                    registerPaint("cornerbox", cornerBox);
                </script>
            

layout API

top

CSS layout API allows to create Display properties. This API extends the layout stage of the browser's rendering pipeline. Layout API will make creating new and complex layouts easy for developers.

The layout worklet needs to be registered before it can be used. Once registered, you can add the layout API in your HTML file using the add method. Finally, use the layout function against the display property in your CSS file.

example: layout API
            registerLayout(
                "mylayout",
                class {
                //code for the layout
                }
            );
            <script>
            CSS.layoutWorklet.addModule('path-to-layout-worklet');  
            </script>
            .container {
                display: layout(mylayout);
            }    
        
example: layout API

This is not quite ready for now. It is not documented on MDN yet, and the implementation will likely change in the future.

code:
                <div class="frame-2">
                    <div class="grid-a">
                    <div class="element-a"></div>
                    <div class="element-a"></div>
                    <div class="element-a"></div>
                    <div class="element-a"></div>
                        <div class="element-a"></div>
                        <div class="element-a"></div>
                        <div class="element-a"></div>
                    </div>
                    <script> 
                        CSS.layoutWorklet.addModule('https://codepen.io/tomquinonero/pen/vYZPbxJ.js');
                    </script>
                </div>
                <style>
                    .frame-2{font-family: sans-serif; height: 50vh; display: grid; place-items: center; background: #f5b4a5;}
                    .grid-a{ display: layout(masonry); --padding: 20; --columns: 2; width: 30vw;}
                    .element-a{--brandingColor: #5f64e2; width: 20vw; height: 5vh; background: var(--brandingColor); border-radius: 4vw;}
                    .element-a:nth-child(2), .element-a:nth-child(6), .element-a:nth-child(3) {height: 10vh;}
                    .element-a:nth-child(4), .element-a:nth-child(5) { height: 1vh;}
                </style>
                <script>
                    registerLayout(
                        "masonry",
                        class {
                            static get inputProperties() {
                                return ["--padding", "--columns"];
                            }
        
                            async intrinsicSizes() {
                            /* TODO implement :) */
                            }
                            async layout(children, edges, constraints, styleMap) {
                            const inlineSize = constraints.fixedInlineSize;
        
                            const padding = parseInt(styleMap.get("--padding").toString());
                            const columnValue = styleMap.get("--columns").toString();
        
                            // We also accept 'auto', which will select the BEST number of columns.
                            let columns = parseInt(columnValue);
                            if (columnValue == "auto" || !columns) {
                                columns = Math.ceil(inlineSize / 350); // MAGIC NUMBER \o/.
                            }
        
                            // Layout all children with simply their column size.
                            const childInlineSize = (inlineSize - (columns + 1) * padding) / columns;
                            const childFragments = await Promise.all(
                                children.map((child) => {
                                return child.layoutNextFragment({ fixedInlineSize: childInlineSize });
                                })
                            );
        
                            let autoBlockSize = 0;
                            const columnOffsets = Array(columns).fill(0);
                            for (let childFragment of childFragments) {
                                // Select the column with the least amount of stuff in it.
                                const min = columnOffsets.reduce(
                                (acc, val, idx) => {
                                    if (!acc || val < acc.val) {
                                    return { idx, val };
                                    }
        
                                    return acc;
                                },
                                { val: +Infinity, idx: -1 }
                                );
        
                                childFragment.inlineOffset =
                                padding + (childInlineSize + padding) * min.idx;
                                childFragment.blockOffset = padding + min.val;
        
                                columnOffsets[min.idx] =
                                childFragment.blockOffset + childFragment.blockSize;
                                autoBlockSize = Math.max(
                                autoBlockSize,
                                columnOffsets[min.idx] + padding
                                );
                            }
        
                        return { autoBlockSize, childFragments };
                        }
                    }
                    );
        
                </script>
            

animation API

top

Animation API improves the performance of web animations by running them on their own worklet. This API is the extension of the Composite stage of the browser's rendering pipeline. The API allows to generate KeyframeEffect animations based on user inputs like a scroll, hover, and click. These animations are non-blocking and more performant than the existing ones since they run off the main thread(on their own worklet).

Similar to the other worklets, the animation worklet needs to be registered first. Then it needs to be added as a module in the main JavaScript file.

example: animation API
Animated using
CSS
Animated using Animation API








code:
                <div class="frame-3"> 
                    <div class="grid-b">
                        <div class="element-b element--css"> Animated using<br>CSS</div>
                        <div class="element-c element--js">
                        Animated using Animation API
                        </div>
                        <div class="controls">
                        <button onclick="playAnimation()">Play animation</button><br><br>
                        <button onclick="pauseAnimation()">Pause animation</button><br><br>
                        <button onclick="slowdownAnimation()">Make animation slower</button><br><br>
                        <button onclick="fastenAnimation()">Make animation faster</button><br><br>
                        <button onclick="reverseAnimation()">Reverse animation</button>
                        </div>
                    </div>
                    <script>
                        CSS.animationWorklet.addModule('https://codepen.io/tomquinonero/pen/WNOWvMP.js');
                        const keyframes = [{
                            transform: 'scale(1)',
                            offset: 0
                        },
                        {
                            transform: 'scale(1.1)',
                            offset: 0.25
                        },
                        {
                            transform: 'scale(1)',
                            offset: 0.50
                        },
                        {
                            transform: 'scale(1.15)',
                            offset: 0.75
                        },
                        ]
                        const timing = {
                        duration: 1200,
                        easing: "linear",
                        iterations: Infinity
                        }
                        const element = document.querySelector('.element--js')
                        const animation = element.animate(keyframes, timing)
                        
                        
                    function pauseAnimation() {
                        animation.pause()
                    }
                        function playAnimation() {
                        animation.play()
                    }
                        function reverseAnimation() {
                        animation.reverse()
                    }
                        function fastenAnimation() {
                        animation.playbackRate = animation.playbackRate * 1.5
                    }
                        function slowdownAnimation() {
                        animation.playbackRate = animation.playbackRate / 1.5
                    }
                    
                    </script>
                </div>
                <style>
                    .frame-3{margin-left: 4vw; font-family: sans-serif; height: 40vh; display: grid; place-items: center; background: #f5b4a5;}
                    .grid-b{display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3.2vw;}
                    .element-b {width: 8vw;height: 8vw; padding: 4vw; text-align: center; font-size: 2vw; color: #f5b4a5; font-weight: bold; 
                        background: #5f64e2; border-radius: 4vw; transform-origin: 50% 50%;}
                    .element--css{animation: bounce 1200ms linear infinite;}
                    @keyframes bounce {
                        0%, 100% {transform: scale(1);}
                        25% {transform: scale(1.1);}
                        50% {transform: scale(1);}
                        75% {transform: scale(1.15);}
                    }
                </style>
                <script>
                    registerAnimator(
                        "superBounce",
                        class {
                            constructor(options) {
                            // Our code goes here
                            }
                            animate(currentTime, effect) {
                            // Our code goes here
                            }
                        }
                        );
                </script>

            

parser worklet

top

Parser worklet extends the parsing stage of the browser's rendering pipeline and is built on top of the Typed Object Model. The goal of this API is to allow the Developer access to the Rendering Engine's Parser. With this API, you can create new rules, perform nesting, extend CSS and apply external properties.

Currently, this API is not implemented; hence the specifications aren't clear. But this API has a massive scope since developers can extend the CSS parsing phase and directly tell the browser what CSS property follows.