Houdini is a set of low-level APIs that exposes parts of the CSS engine, giving developers the power to extend CSS by hooking into the styling and layout process of a browser’s rendering engine. Houdini is a group of APIs that give developers direct access to the CSS Object Model (CSSOM), enabling developers to write code the browser can parse as CSS, thereby creating new CSS features without waiting for them to be implemented natively in browsers.
It really is magic, hence it's name Houdini. I'd recommend this slidedeck and this video to get you started
If you’re interested into making your first Paint Worklet with Houdini, you can easily follow along with the article/tutorial.
When using CSS Custom Properties we mainly use them directly as variables in calculations for other properties. Having one CSS Custom Property control a varying set of other properties — such as both colors and numbers — is not exactly possible. There are some hacky workarounds we can use, but these don’t cover all scenarios. Thankfully there’s a new idea popping up: Higher Level Custom Properties. Although still premature, these Higher Level Custom Properties would allow us to drop the hacks.
Let’s take a look at our current options, and how this (possible) future addition to the CSS spec — along with the @if at-rule it introduces — might look …
Using the var() function we create a CSS Variable which gets substituted for the value of the Custom Property it refers to.
E.g. The variable var(--square-size) will hold the value of the --square-size Custom Property — namely 2vw — which is then set as the value for the width CSS property.
🤔 CSS Custom Properties vs. CSS Variables — Is there a difference?
A CSS Custom Property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. Just like with a normal property you can assign a value to it, e.g. --foo: 200;.
A CSS Variable is created when the var() function is used. When creating the CSS Variable var(--my-prop), it will be replaced with the value of the --my-prop Custom Property it refers to, namely 200.
~
# Using CSS Custom Properties to affect multiple CSS declarations
In the example above we have two types of squares: regular sized ones and big ones. To differentiate between them we need to toggle the .square--big class. Toggling that class affects two CSS Custom Properties: both --square-size and --square-padding are altered.
But what if we wanted not to toggle a HTML class but a CSS Custom Property to do so? E.g. we want to toggle one CSS Custom Property, and have that automatically affect both --square-size and --square-padding.
As it stands today it’s not very straightforward to let one single CSS Custom Property affect multiple other CSS Properties, unless you resort to some hacky workarounds. Let’s take a look at the options we have today.
If all you’re setting is numeric values, you can use Binary CSS Custom Properties within calculations. You give these Binary Custom Properties the value of 0 or 1 and use them within your calculations. Think of these Binary Custom Properties like light switches: they can either be OFF/false (0) or ON/true (1).
:root {
--is-big: 0;
}
.square--big {
--is-big: 1;
}
.square {
width: calc(
2vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
+
16vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
);
padding: calc(
0.25vw * (1 - var(--is-big)) /* Value to apply when --is-big is 0 (~false) */
+
1vw * var(--is-big) /* Value to apply when --is-big is 1 (~true): */
);
aspect-ratio: 1/1;
}
In the example above the --is-big Custom Property acts as a binary toggle that controls the results of the calc() functions. In the case of --is-big having a value of 0 those functions will yield one specific value, while when --is-big is set to 1 it will yield another value.
☝️ With some extra effort you can even perform Logical Operations (AND, NAND, OR, NOR, XOR, …) using CSS Custom Properties!?
When you need to set things other than numeric values — such as colors — you can’t rely on a toggle that is either 0 or 1, as performing calculations with colors is invalid.
What you can do however is use The CSS Custom Property Toggle Trick by James0x57 — which I like to call “The Guaranteed-Invalid Value Hack” — where you set a Custom Property to the “guaranteed-invalid value” of initial to force the var() function to use its fallback value:
If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.
--my-var: initial; /* initial => var() will use the fallback value */
color: var(--my-var, green); /* ~> green */
--my-var: hotpink; /* Any value other than `initial` (even simply one space!) => var() will not use the fallback value */
color: var(--my-var, green); /* ~> hotpink */
That means that you can flip the switch ON by setting a Custom Property to the value of initial. Here’s an example where the text will turn green and italic once --is-checked is flipped on:
A limitation of this approach however is that you can’t define several values to use in case --is-checked is in the OFF state. Say I want the text in the example above to be both red by default and with a border. Setting --is-checked to red will only get me halfway, as that value is only valid for the color property here.
input[type="checkbox"] + label {
--is-checked: red; /* Default value to use */
color: var(--is-checked, green); /* ✅ Will be red by default */
border: var(--is-checked, none); /* ❌ What about a default value for border? */
font-style: var(--is-checked, italic); /* ❌ What about a default value for font-style? */
}
🚨 Do note that this proposal is still in it’s very very early stages and part of an ongoing discussion. The CSS WG has merely expressed interest in this proposal, suggesting that it should be explored further. If if tends to be helpful and even possible, only then work on a Working Draft will start.
“Higher Level Custom Properties” are Custom Properties that control a number of other CSS Properties. As the proposal stands right now you use them in combination with a newly proposed @if at-rule, like so:
Unlike the Custom Properties we know today, a Higher Level Custom Property controls multiple declarations, way beyond simple variable substitution. In the example above we set our HLCP --size to have a value of big. This value isn’t used directly, but affects the other properties width and padding.
Using this HLCP also improves the meaning of our code. Setting width: 16vw; does not clearly express our intent, whereas setting --size: big; does.
💁♂️ If you don’t like @if then please don’t discard the whole idea immediately, but focus on the problem it’s trying to fix here. Lea’s proposal is a possible solution, not the solution. Could be that — in the end — we end up with a totally different syntax.
Before you get too excited, there are still some cases that need to be taken care of. In a follow-up comment on the proposal, Lea documented some already identified issues.
🚨 Note that these issues are blocking issues. As long as these aren’t resolved, HLCPs won’t happen.
A first issue is a problem with the desugaring of @if and partial application. Behind the scenes a @if at-rule desugars to the still discussed if() function call. The example above eventually becomes this:
Now consider that an element that matches .foo is inside a 600px by 400px container and has a computed font-size of 25px; This makes 1em > 5% evaluate to false on the width property and true on the height property, which would make the @if partially applied. We most definitely don’t want that.
There are some ideas floating around to fix this — such as forcing percentages/lengths to always be compared against the width — but that’s still a bit vague right now.
Another issue that was pointed out is one on Cascading. I especially like this one, as it gives us a good insight in how CSS behaves and works:
Inline conditionals will have the IACVT (Invalid At Computed Value Time) behavior that we have come to know and love (?) from Custom Properties. Since @if will desugar to inline conditionals, it will also fall back to that, which may sometimes be surprising. This means that these two snippets are not equivalent:
A declaration can be invalid at computed-value […] if it uses a valid custom property, but the property value, after substituting its var() functions, is invalid. When this happens, the computed value of the property is either the property’s inherited value or its initial value […].
This explains why in the example below the background won’t be red but (the default)transparent.
:root { --not-a-color: 20px; }
p { background-color: red; }
p { background-color: var(--not-a-color); }
👉 As 20px is no valid <color> value, the last declaration will become background-color: initial;.
💡 If we would have written background-color: 20px directly (e.g. without the use of Custom Properties), then that declaration would have simply been discarded due to being invalid, and we would have ended up with a red background.
The “Higher Level Custom Properties” idea by Lea Verou is one that quite excites me, as it solves an actual issue one can have in their code and would avoid having to use one of the nasty hacks.
There’s still a long way to go before we might actually see this land, yet as the CSS WG has expressed interest I’m hopeful that the already identified issues will be wrinkled out, and that work on an official spec can start.
Anthony Ricaud, writing for the Web Performance Calendar, on the extra load that JavaScript can put on your site, and how you can replace some things with basic HTML and CSS:
Relying on solutions provided natively by browsers enables you to benefit at low cost from the expertise of the community creating web standards. These solutions generally have the advantage of using less code, thus reducing maintenance efforts for a development team (for example, no need to update the libraries used).
In this article, we will explore some of these native solutions that are available to the majority of your users.
Adam Argyle looking at several techniques to center in CSS, and how they hold up in several conditions (narrow screen, rtl, etc)
In today’s challenge, we’re stress testing 5 different CSS centering techniques. See what techniques should earn a place in your tool belt by watching how they react to common layout stress. The contestants: content center, gentle flex, autobot, fluffy center, and pop & plop.
If you prefer a written format, a full write-up is also available on web.dev
Earlier this week I saw this tweet by Sansec float by:
After finding skimmers in SVG files last week, we now discovered a #magecart skimmer in perfectly valid CSS. It is parsed and executed during checkout. Malware loaded from cloud-iq[.]net (faking @cloudIQApps) pic.twitter.com/Hy6b6lxJoL
This one’s pretty nice I must say: as the syntax for CSS Custom Properties is overly permissive (see here) you can use Custom Properties to store your JavaScript attack vector in. If you then use window.getComputedStyle to extract the contents of the Custom Property (see here) and combine it with a function constructor and an IIFE, it’s possible to execute it.
Here’s a pen that loads a remote confetti script using the method described:
Let this underline the importance of a Content Security Policy to prevent remote script loading script evaluation.
Update: Blocking this “hack” with a proper CSP
It took me some time to figure out — as I’m no CSP expert — but turns out the unsafe-inline keyword in the CSP’s source list is enough to block the execution of the JS-IN-CSS.
As a reminder, here are the four allowed keywords:
'none', as you might expect, matches nothing.
'self' matches the current origin, but not its subdomains.
'unsafe-inline' allows inline JavaScript and CSS.
'unsafe-eval' allows text-to-JavaScript mechanisms like eval.
I first thought unsafe-inline would be insufficient here as the code does not call eval, but apparently a function constructor is (correctly!) considered equally harmful, and therefore also blocked.
Here’s an updated demo that blocks the script evaluation:
This is because elements will be rendered as they scroll into the viewport and will be hidden as they scroll out of the viewport, thereby affecting the height of the rendered page, and thus also affecting the scrollbar.
ℹ️ Apart from a jumpy scrollbar it can also negatively affect accessibility when you include headings and landmark elements inside of regions styled with content-visibility: auto;. See Content-visibility and Accessible Semantics for details.
Now, thanks to infinite scroll we are — or at least I am — kind of used to the thumb part of the scrollbar shrinking and jumping back up a bit on the scrollbar track as you scroll down. What we’re not used to is the thumb part jump forwards on the scroll track as you scroll down. This is because elements that slide out of the viewport will no long be rendered — as that’s what content-visibility: auto; does — and the scrollbar is (by default) only calculated against the rendered elements.
Elements can become non-rendered elements as they scroll out of the viewport, thanks to content-visibility: auto; doing its thing.
To cater for this jumpy behavior you should use contain-intrinsic-size so space for an element is reserved when it’s not being rendered. However, it is not always possible to know a box its dimensions in advance. Looking for a way to automatically reserve space for previously rendered elements, Alex Russel created a little script for it.
One challenge with naive application of content-visibility, though, is the way that it removes elements from the rendered tree once they leave the viewport — particularly as you scroll downward. If the scroll position depends on elements above the currently viewable content “accordion scrollbars” can dance gleefully as content-visibility: auto does its thing.
In a first version of the script he applied content-visibility: visible on each element from the moment it had appeared on screen. To detect this an IntersectionObserver is used. While this does prevent the scrollbar thumb from jumping forwards as you scroll down, it will make the page slow again as that content remains rendered (even though it’s off-screen).
A second version of the script takes a different approach and calculates the contain-intrinsic-size to apply based on the element’s dimensions. That way elements that passed by once now have a correct contain-intrinsic-size set, and can safely be hidden again as content-visibility: auto does its job.
let spaced = new WeakMap();
let reserveSpace = (el, rect) => {
let old = spaced.get(el);
// Set intrinsic size to prevent jumping.
if (!old || rectNotEQ(old, rect)) {
spaced.set(el, rect);
el.attributeStyleMap.set(
"contain-intrinsic-size",
`${rect.width}px ${rect.height}px`
);
}
};
Additionally he also added a ResizeObserver to cater for resize events.
🤔 Clever script indeed, yet I cannot help but think: this should be possible without the needs for this extra script. What if a value like contain-intrinsic-size: auto; would be allowed, and do exactly as the script Alex built does?
Side note: I’m proud to have been asked to review this book while it was still in the making, and am glad I was of help to Ahmad. Great job you did there!
If you’ve ever tried to put a sticky item in a grid layout and watched the item scroll away with the rest of the content, you might have come to the conclusion that position: sticky doesn’t work with CSS Grid. Fear not! It is possible to get these two layout concepts working together. All you likely need is one more line of CSS.