Bram.us: Recent Episodes

None

A rather geeky/technical weblog, est. 2001, by Bramus

View Details

Me, on stage.Last month I spoke at React Brussels and gave a talk “Supercharge Web UX with View Transitions”

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave is a full-length talk of a little over 30 minutes.

Tired of disjointed web apps? View Transitions are the game-changer you’ve been waiting for. Whether your app is single or multi-page, this powerful API lets you create seamless, native-like experiences that captivate users. Join me as I dive into the world of View Transitions, showing you how to replace jarring page loads with elegant transitions. Learn to harness the flexibility of CSS and the power of JavaScript to customize transitions and create a truly unique experience. If you’re ready to take your web apps to the next level, this talk is a must-attend.

~

SlidesThe slides of my talk are up on slidr.io are embedded below:

Watch the slides

These exported slides don’t contain any recordings of the demos included, but you can follow the link to check them out yourself. For the Same-Document View Transitions demos you will need Chrome 111+. For the Cross-Document View Transitions demos you need Chrome 126+.

~

Recording/VideoThis talk was recorded and is available for you to watch on YouTube. The video is also embedded below:

~

Thanks!Thanks to Aymen and Elian for inviting me to speak at this wonderful event. It was very well organized and everything – from my POC – went smooth. A pity the turnout wasn’t that great (but definitely not bad too!). It was also very heartwarming to see that not all talks focused on purely React itself and some of its libraries, but that some talks also covered things such as accessibility and progressive enhancement. A well balanced set of talks, delivered by some great speakers, is what I consider a great conference 🙂

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

Yesterday I published a new version of my experimental Chrome Dark Mode Toggle extension. On top of a per-origin override, you can now set a Chrome-wide preference to have your OS in Dark Mode but all sites in Light Mode (or vice versa).

You can get the extension from the Chrome Web Store.

For a backstory behind the why and how of this extension, go read the original announcement post.

View Details

https://www.bram.us/wordpress/wp-content/uploads/2024/10/ssstwitter.com_1728373297963.mp4Recording of Paul’s demoDissecting and reworking a very nice demo by Paul Noble.

~

Card Stack DemoPaul Noble created an AMAZING scroll-driven animations demo in which you can drag cards from a card stack. As he describes it:

Card stack using scroll-driven animation w/ snapping. Just a few lines of JS, zero dependencies.

Try it out (in Chrome) right here:

See the Pen
Scroll-driven animated card stack with scroll snap events. by Paul Noble (@paulnoble)
on CodePen.

The logic/math used for the stack is based on this thread by Nate Smith

~

💁‍♂️ Unfamiliar with Scroll-Driven Animations? Don’t worry, I’ve created a free video course “Unleash the Power of Scroll-Driven Animations” which teaches you all there is to know about Scroll-Driven Animations.

Go check it out to become an expert in Scroll-Driven Animations!

~

Dissecting the code On social media I already shared (Twitter, Mastodon) how Paul built it:

Invisible ScrollerThe first clever thing Paul is doing there is using an invisible scroller laid on top of the stack of cards. This scroller gets a ScrollTimeline named --scroll-timeline attached to it.Hoisting the TimelineTo make the .card-stack see the --scroll-timeline, it gets hoisted using timeline-scope on the body. Any child of the body – including .card-stack – can therefore use that --scroll-timeline.Scroll-SnappingTo make the cards always show at the center, CSS scroll-snapping is used.Snap EventsIn his demo, Paul also uses the new scrollsnapchange event to let the markup know which card has snapped. This is propagated through the data-active-index attribute on the main element.Custom animations based on which card has snapped Based on the data-active-index attribute, different animations are attached to the cards: the snapped card gets an active animation – which rotates in 3D around the stack – and the non-snapped cards get an inactive animation – which rotates the card around its base.~

Remixing the demoPaul’s demo is amazing but also hard to read because the Sass code uses quite some some math to generate keyframes for each card. As hinted on social media I was quite sure that the effect can also be done using shared keyframes for each card. To attach the keyframes to a single .card a ViewTimeline on the linked .scroll-item can be used, and using animation-range it’s possible to limit when the animation should run.

Yesterday evening I put my money where my mouth is and took my idea for a spin. The result is not 100% perfect – there are some 3D stacking issue, most likely I need to tweak the animation-ranges a bit more – but the result comes pretty close to the original:

See the Pen
Scroll-driven animated card stack with scroll snap events (Vanilla) by Bramus (@bramus)
on CodePen.

While at it, I also reworked the scrollSnapChange logic to use event.snapTargetInline and by also providing a fallback using IntersectionObserver in browsers with no support for the Snap Events.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

I have a new article up on web.dev, about CSSNestedDeclarations which is coming to all browsers.

To fix some weird quirks with CSS nesting, the CSS Working Group resolved to add the CSSNestedDeclarations interface to the CSS Nesting Specification. With this addition, declarations that come after style rules no longer shift up, among some other improvements.

These changes are available in Chrome from version 130 and are ready for testing in Firefox Nightly 132 and Safari Technology Preview 204.

Besides writing the post, I had lots of fun building this CSSRule debugger for this blogpost. It shows you what goes on behind the scenes and how your CSS gets interpreted by the CSS Engine.

Here’s a comparison of Chrome without and with CSSNestedDeclarations support. The version with CSSNestedDeclarations support clearly is better.

Screenshot of the CSSRule Debugger in Chrome 129, which has no CSSNestedDeclarations support: in the parsed CSS the declarations got shifted up, amongst other things.Screenshot of the CSSRule Debugger in Chrome 130, which has CSSNestedDeclarations support: the parsed CSS is the same as the input CSS thanks to CSSNestedDeclarations which you can see in the serialized CSS section.I cannot help but stress that this is a change that is part of the CSS Nesting spec and is one that is coming to all engines. Firefox Nightly 132 is passing all tests, and with 8/11 subtests passing Safari Technology Preview 204 still has a little bit of cleaning to do before it can ship this.

Read “CSS nesting improves with CSSNestedDeclarations” →

View Details

The other day on X, Adam Wathan wondered how to feature detect (Custom Property) Style Queries. While in theory you could use @supports at-rule() for this, in practice you can’t because it has no browser support (… yet).

Drawing inspiration from my previous post on how to detect support for @starting-style, I came up with a similar method to detect support for Style Queries: try actively using them and respond to that.

~

The CodeIf you are here for just the code, here it is:

html { --sentinel: 1;}@container style(--sentinel: 1) { body { --supported: ; /* Space Toggle */ }} With this you get a Space Toggle for you to use in your code.

Before you tl;dr this post, you might still want to read The problem with Safari 18 section …

~

How it worksThe code works by actively trying to use a Style Query. It sets a --sentinel property on the root element and then lets the body element respond to it – using a Style Query – trying to declare the --supported custom property.

When Style Queries are supported, the result will be a --supported custom property that is set to an empty value. In browsers with no Style Queries support, the --supported property will be the guaranteed-invalid value (of initial). Yes, a Space Toggle.

With that --supported Space Toggle in place, you can then use it in your CSS:

body { --bg-if-support: var(--supported) green; --bg-if-no-support: var(--supported, red); background: var(--bg-if-support, var(--bg-if-no-support));} ~

DemoThe demo below uses the code shown earlier:

See the Pen
Feature Detect Style Queries (1/2) by Bramus (@bramus)
on CodePen.

If you’re using Safari 18, you might notice it doesn’t work as expected …

~

The problem with Safari 18While Safari 18 does come with support for (Custom Property) Style Queries, you might have noticed the previous demo does not work in it.

The culprit: A bug in which the root element cannot be a container in Safari 18 – https://bugs.webkit.org/show_bug.cgi?id=271040.

To work around this bug, you need to move everything down one level in your DOM tree. Like so:

body { --sentinel: 1;}@container style(--sentinel: 1) { body > * { --supported: ; /* Space Toggle */ }} This means you can’t use --supported to conditionally style the body element itself, which might be OK for your use-case.

☝️ The bug has been fixed in Safari Technology Preview 204 and should be included in Safari 18.1 when released.

~

Safari 18-friendly demoHere’s a demo of the code that also works in Safari. Note that it can’t be used to style the body element itself.

See the Pen
Feature Detect Style Queries (2/2) by Bramus (@bramus)
on CodePen.

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

📝 Feature detect Style Queries Support in CSS.

Awaiting browser support for at-rule(), here’s how you do it.https://t.co/uXikYbBM2S pic.twitter.com/iuyVwlAgyX

— Bram.us (by @bramus) (@bramusblog) October 6, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

With @​property now being Baseline Newly Available, I thought it’d be a good time benchmark the impact – if any – it has on the performance of your CSS.

When starting to use a new CSS feature it’s important to understand its impact on the performance of your websites, whether positive or negative. With @property now in Baseline this post explores its performance impact, and things you can do to help prevent negative impact.

For this I built and open sourced the “CSS Selector Benchmark” project which I have been working on for some time now.

To benchmark the performance of CSS we built the “CSS Selector Benchmark” test suite. It is powered by Chromium’s PerfTestRunner and benchmarks the performance impact of CSS. This PerfTestRunner is what Blink (= Chromium’s underlying rendering engine) uses for its internal performance tests.

The runner includes a measureRunsPerSecond method which is used for the tests. The higher the number of runs per second, the better.

The created benchmarks for @property specifically measure how fast Blink can handle a Style Invalidation and the subsequent Recalculate Style task. This was tested with both registered and unregistered custom properties, as well as regular properties.

Read “Benchmarking the performance of CSS @property” on web.dev →
Check out “css-selector-benchmark” on GitHub →

View Details

https://www.bram.us/wordpress/wp-content/uploads/2024/09/hide-on-scroll-down-show-on-scroll-up.mp4Recording of Fabrizio’s demoBy adding a long transition-delay to a CSS property under certain conditions (which you can do using a Style Query), you can persist its value.

~

Detecting the Scroll-Direction with CSS Scroll-Driven AnimationsOne of the demos that I built as part of the “Solved by CSS Scroll-Driven Animations: Style an element based on the active Scroll Direction and Scroll Speed” article is a header element that hides itself on scroll.

Here’s the demo I’m talking about: as you scroll up or down, the header hides itself. When idling, it comes back into view. Check it out using a Chromium-based browser, as those – at the time of writing – are the only browsers to support Scroll-Driven Animations.

See the Pen
CSS scroll-direction detection with Scroll-Driven Animations with moving header by Bramus (@bramus)
on CodePen.

In the code of that demo there are few CSS variables that are either 0 or 1 when scrolling – or not-scrolling – in a certain direction. The CSS looks like this:

--when-scrolling: abs(var(--scroll-direction)); --when-not-scrolling: abs(var(--when-scrolling) - 1); --when-scrolling-up: min(abs(var(--scroll-direction) - abs(var(--scroll-direction))), 1); --when-scrolling-down: min(var(--scroll-direction) + abs(var(--scroll-direction)), 1); --when-scrolling-down-or-when-not-scrolling: clamp(0, var(--scroll-direction) + 1, 1); --when-scrolling-up-or-when-not-scrolling: clamp(0, abs(var(--scroll-direction) - 1), 1); 💁‍♂️ If you want to know exactly how it works, go check out episode 9 of the free video course “Unleash the Power of Scroll-Driven Animations” I made, which teaches you all there is to know about Scroll-Driven Animations. The episode is also right here:

~

The transition-delay trickAs I had noted in the article, these variables are fleeting. From the moment you stop scrolling, they all – except --when-not-scrolling become 0 again. Therefore, the header in the example will show again I wanted to extend the functionality so that – for example – the header hides when scrolling down and for it to remain hidden until you only scroll up again.

Fast forward to a few months later. While at CSS Day 2024, Schepp shared that he found way to make those custom properties “sticky”. His trick? Adding a long transition-duration to the properties when scrolling in a certain direction.

In the following snippet, the transition is stalled indefinitely when idling. That way, the --scroll-* custom properties will retain their value until you start scrolling again.

@container style(--scroll-direction: 0) { header { transition-delay: calc(infinity * 1s); }} ~

Putting it all togetherUnfortunately I hadn’t found the time to actively use Schepp’s suggestion in the hiding header demo ever since we discussed it (but I did use it for my @starting-style feature detection technique).

Fast forward to just last week, and Fabrizio Calderan reached out on X to share his “Hide on Scroll Down, Show on Scroll Up Header” CodePen

See the Pen
Hide on Scroll Down, Show on Scroll Up Header by Fabrizio Calderan (@fcalderan)
on CodePen.

Fabrizio came to creating the same trick Schepp had suggested to me, by relying on a long transition-behavior which he sets in a Style Query:

@container style(--scroll-direction: 0) { /* Scroll is idle, so we keep the current header position by setting the transition-delay to infinity */ header { transition-delay: calc(infinity * 1s); }}@container style(not (--scroll-direction: 0)) { /* page is scrolling: if needed, the animation of the header should run immediately */ header { transition-delay: 0s; }}@container style(--scroll-direction: -1) { /* Scrolling up, so we must reveal the header */ header { --translate: 0; }}@container style(--scroll-direction: 1) { /* Scrolling down, so we must hide the header */ header { --translate: -100%; }} Nice one, Fabrizio!

When trying it out, you’ll notice it still is not 100% perfect though, as you can end up in situation where the header remains hidden when starting a scroll down immediately followed by a scroll up. This confirms to me that there still is a need to have the scroll-direction be exposed by the browser itself, instead of needing to rely on a hack powered by Scroll-Driven Animations. The current line of thinking is to use a Scroll-State Style Query for this.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

Last week I joined my colleagues Adam and Una on The CSS Podcast. I was brought on to talk about View Transitions, a feature I’m doing the DevRel work for at Google.

In this episode Una and Adam bring on an esteemed guest Bramus, who brings us deep knowledge on View Transitions. These are easy to get started with but difficult to master, but not with Bramus here to teach us. He’ll be covering introductory to advanced API features and a big bag of examples and demos.

You can watch the episode on YouTube, which I have embedded below:

You can also listen to it using your favorite podcasting app.

View Details

Me, on stage. Photo by devs.gent.Today I gave a talk at the September 2024 devs.gent meetup on how to observe and respond to Style Changes.

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave was about half an hour and covered my journey into building @bramus/style-observer

A shortcoming of MutationObserver is that it cannot be used to subscribe to value changes of CSS properties.

While you could resort to requestAnimationFrame and getComputedStyle to plug that hole (which you shouldn’t), there is a more performant way to achieve this: leverage the power of CSS transitions in combination with the fairly recent transition-behavior: allow-discrete. With it, you can set up JavaScript callbacks to respond to changes in computed values of CSS properties – including Custom Properties.

This is basically a talkified version of this blog post.

~

SlidesThe slides of my talk are up on slidr.io are embedded below:

Watch the slides

These exported slides don’t contain any recordings of the demos included, but you can follow the links on the slides to check them out yourself.

~

Recording/VideoThis talk was recorded. Once the recording is released, I’ll update this post to include the embed.

~

Thanks!Thanks again to Bert, Elian, and Freek for having me. Always a pleasure to speak at a local meetup and meet new and old friends. Also a big kudos to Lemon for hosting, offering food and beverages, and providing a crew to record the talks.

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

~

Scroll-Driven Animations versus FirefoxAhmad recently launched a redesign of his website. On it he features some nice Scroll-Driven Animations to enhance the experience. As one should do, the CSS for it is gated behind an @supports result which feature detects availability for Scroll-Driven Animations.

@supports (animation-timeline: scroll()) { /* Scroll-Driven Animations related styles go here */} However, even though he gated the functionality behind a feature check, he got some reports from users that it wasn’t working as expected in Firefox: the animations would run on scroll but the timing would be waaaayy off. So he reached out to me, asking what gives.

The problem is that, at the time of writing, Firefox Nightly – which has Scroll-Driven Animations enabled – only has a partial implementation of it. They support a bit of it, but not everything. One of the things they do support is animation-timeline: scroll(), making Firefox Nightly pass the feature detection snippet from above.

~

Filtering out Firefox’s partial implementationTo filter out Firefox Nightly, you need to extend the feature detection for Scroll-Driven Animations to include a check for something they don’t support yet. For this, you can check for animation-range support, as that’s a property that is not part of their partial implementation.

Like so:

@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) { /* Scroll-Driven Animations related styles go here */ /* This check excludes Firefox Nightly which only has a partial implementation at the moment of posting (mid-September 2024). */} Here’s a live demo:

See the Pen
Feature detect scroll-driven animations support but exclude Firefox’s partial implementation by Bramus (@bramus)
on CodePen.

That’s it 🙂

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

Feature detecting Scroll-Driven Animations with @​ supports in CSS: you want to check for animation-range too, in order to exclude Firefox Nightly, which only has a partial SDA implementation.

Details: https://t.co/K0OMGj5olj pic.twitter.com/HIcRBV5yzM

— Bram.us (by @bramus) (@bramusblog) September 24, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

The current state of View Transitions is nice, but it could be even better …

~

The clip-path problemAn area where View Transitions fall short is how it handles clip-path, border-radius, opacity, …

Take this demo, which has a view-transition-name on the grey circle and a view-transition-name on the text contained within that circle.

https://www.bram.us/wordpress/wp-content/uploads/2024/09/view-transitions-capture-mode-before.mp4As you can see in the recording, the snapshots cross-fade whereas you’d most likely want the clip-path of the circle to actually animate instead.

Also, the nested text bleeds out of its container; this because the snapshots are a flat list instead of a nested tree:

::view-transition├─ ::view-transition-group(card)| └─ ::view-transition-image-pair(card)| ├─ ::view-transition-old(card)| └─ ::view-transition-new(card)└─ ::view-transition-group(text) └─ ::view-transition-image-pair(text) ├─ ::view-transition-old(text) └─ ::view-transition-new(text) While this outcome of these View Transition limitations could be acceptable in some cases, here it is not.

~

It can be betterBut what if this issues could be solved? So that you’d get the following instead:

– The clip-path on the circle actually transitions
– The text stays clipped by the circle

Like so:

https://www.bram.us/wordpress/wp-content/uploads/2024/09/view-transitions-capture-mode-after.mp4That’s much better, right?

To achieve this, there are 2 changes needed for View Transitions:

  1. A way to preserve the tree when building the View Transition pseudos.
  2. A new capture mode that captures certain properties (such as clip-path, border-radius, opacity, …) individually and copies them onto the ::view-transition-group().

Combine the two, and you are able to get what you see in that second recording there 🙂

~

It will be betterAt the time of writing, Chrome is actively working on implementing these two new features, gated behind a feature flag. To try these out, you need Chrome Canary with Experimental Web Platform Features turned on.

  1. The new view-transition-group property – part of the View Transitions L2 specification – allows you to determine where to place the snapshot. In the demo shown before, I have the text’s pseudos be nested inside the box group pseudo, like so:

.card {view-transition-name: card;}.card a {view-transition-name: text;view-transition-group: card; /* 👈 Make the pseudo-tree for this “text” snapshot to be nested inside the “card” pseudo-tree */}

::view-transition└─ ::view-transition-group(card) ├─ ::view-transition-image-pair(card) | ├─ ::view-transition-old(card) | └─ ::view-transition-new(card) └─ ::view-transition-group(text) └─ ::view-transition-image-pair(text) ├─ ::view-transition-old(text) └─ ::view-transition-new(text) You can also use a value of nearest to make it less explicit 2. As for the capturing, Chrome is experimenting with a new capture mode that animates a select set of properties directly onto the ::view-transition-group().

The set of properties we are looking at are clip-path, border-radius, opacity, mask, filter, and clip properties.

You can already try this out in Chrome Canary but note that the implementation still needs some work as the border-radius and clip properties aren’t implemented yet.

If all goes well, this new mode will eventually replace the old mode.

Pretty sweet, right?

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

I have a new article up on developer.chrome.com:

An often requested CSS feature is the ability to animate to height: auto. A slight variation of that request is to transition the width property instead of the height, or to transition to any of the other intrinsic sizes represented by keywords like min-content, max-content, and fit-content.

As of Chrome 129 you can now use the interpolate-size property or the calc-size() function to enable smooth transitions and animations from lengths to intrinsic sizing keywords and back.

The gist is to add the following snippet to your stylesheet to opt-in to interpolating from <length-percentage>s to any of the supported <intrinsic-size-keyword>s or vice versa.

:root { interpolate-size: allow-keywords;} The alternative is to use the calc-size() approach (which nowadays requires two arguments btw), but in most cases interpolate-size: allow-keywords; will do just fine.

Find all the details and a bunch of demos in the article Animate to height: auto; (and other intrinsic sizing keywords) in CSS.

View Details

Last week I scratched my own itch (again) and built a (offline!) command line tool for “CanIUse …” and MDN’s browser-compat-data.

~

Installation and UsageYou can install @bramus/caniuse-cli through NPM (or whatever package manager you are using).

npm i -g @bramus/caniuse-cli Once installed, you can call caniuse on the command line. The passed in argument is your search term.

caniuse viewport-units The package differs from other attempts at this by not only using caniuse-db but by also integrating @mdn/browser-compat-data. That means you can also query for things like the upcoming calc-size() or @property.

caniuse calc-size

caniuse @property Because both datasets are stored locally, @bramus/caniuse-cli requires no live internet connection once installed.

Furthermore it also collapses the releases in the output table, just like the “CanIUse …” website does it.

~

Source codeThe source code is available on GitHub. PRs very welcome as this was thrown together in a jiffy, hacking on the original by @dsenkus.

One area I am specifically looking help for, is a way to provide an auto-update mechanism for the datasets. I think I have a solution for this, but I’m sure it could be done better – especially because my approach is limited to Node’s npm only.

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

I scratched my own itch (again) and built a @caniuse CLI tool.

It differs from other attempts at this by also integrating @MozDevNet’s browser-compat-data + collapsing releases in the table (just like the website does).

⌨️npm i -g @bramus/caniuse-cli
🔗 https://t.co/Ocp3t9erEz pic.twitter.com/Ly6SPNkgKo

— Bramus (@bramus) September 9, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

A shortcoming of MutationObserver (imho) is that it cannot be used to subscribe to value changes of CSS properties.

To plug that hole I built a library allowing just that: @bramus/style-observer. It allows you to attach JavaScript callbacks to changes in computed values of CSS properties.

It differs from previous attempts at doing this by not relying on requestAnimationFrame and by supporting properties that animate discretely (which includes Custom Properties). To achieve this, the library is powered by CSS Transitions and transition-behavior: allow-discrete.

~

DemoLet’s jump straight in with a demo. Every time you click the document, the background-color is set to a random color. In response to each change, thanks to @bramus/style-observer, a callback gets executed. This callback shows the new computed value in a notification using the notyf library.

See the Pen @bramus/style-observer demo by Bramus (@bramus) on CodePen.

~

Installation and UsageTo obtain @bramus/style-observer install it through NPM (or whatever package manager you are using):

npm install @bramus/style-observer The following code shows you how to use it. See the inline comments for explanation on what each section does:

// Import the CSSStyleObserver classimport CSSStyleObserver from "@bramus/style-observer";// Array with the names of the properties to observe.// This can be one or more, both regular properties and custom propertiesconst properties = ['background-color'];// Create a CSSStyleObserver that tracks the properties.// Every time one of those properties their computed value changes, the passed in callback gets executed// Here, the callback shows the new computed value in a notificationconst cssStyleObserver = new CSSStyleObserver(properties,(values) => {showNotification(values['background-color']);});// Have the CSSStyleObserver instance observe the `<body>` elementcssStyleObserver.attach(document.body);// Change the background-color every time you click the documentdocument.documentElement.addEventListener('click', (e) => {document.body.style.setProperty('background-color', randomBackgroundColor());}); ~

Under the hoodUnder the hood @bramus/style-observer relies on CSS Transitions and transition-behavior: allow-discrete;.

Each property that you monitor with @bramus/style-observer gets a short CSS Transition applied to it. The transition is set to a very short transition-duration of 0.001ms and the transition-timing-function is set to step-start so that the transition immediately kicks in.

To catch this transition, the library also sets up a transitionstart event listener which invokes the callback that was passed into the CSSStyleObserver.

In CSS, transitions normally only fire for properties that can be interpolated. This does not include properties that animate discretely. Thanks to the very recent transition-behavior, it is now possible to have transitions – along with their events – on properties that animate discretely – which includes Custom Properties – after all. This is achieved by declaring transition-behavior: allow-discrete; onto the monitored element.

~

Browser SupportTechnically speaking, @bramus/style-observer works in any browser that supports CSS Transitions. To also observe properties that animate discretely, support for transition-behavior: allow-discrete; is also required so in practice that boils down the the following browsers that are supported:

  • Chrome/Edge 117
  • Firefox 129
  • Safari 18

Note that all the browsers have bugs when transitioning Custom Properties. See the next section for details.

~

A note on transitioning Custom PropertiesThere are a bunch of browser bugs – in all browsers – when it comes to transitioning custom properties.

Chrome for example is currently affected by https://crbug.com/360159391 in which it does not trigger transition events for unregistered custom properties. You can work around this Chrome bug by registering the custom property using @property.

Safari doesn’t like the Chrome workaround for certain syntaxes as it then seems to be stuck in a transition loop. This happens when the custom property is not registered or when the custom property is a string ("<string>", "*"/…). Other syntaxes – such as "<number>" and "<custom-ident>" – don’t mess up things in Safari (and also bypass that Chrome bug).

And Firefox finally doesn’t like it when a registered custom property uses a syntax with a type that can be interpolated.

UPDATE 2024.09.02 – I have gathered all issues on a dedicated site at https://allow-discrete-bugs.netlify.app/.

Right now, the only cross-browser way to observe Custom Properties with @bramus/style-observer is to register the property with a syntax of "<custom-ident>".

Note that <custom-ident> values can not start with a number, so you can’t use this type to store numeric values.

~

Prior Art and AcknowledgementsThis section is purely informational.

The requestAnimationFrame daysWanting a Style Observer is not a new idea. There have been attempts at making this before such as ComputedStyleObserver by keithclark (2018) and StyleObserver by PixelsCommander (2019).

Both rely on using requestAnimationFrame, which is not feasible. This because requestAnimationFrame callbacks get executed at every frame and put a load on the Main Thread.

Furthermore, the callback used in those libraries would also typically trigger a getComputedStyle and then loop over all properties to see which values had changed, which is a slow process.

Besides putting this extra load on main thread, looping over the getComputedStyle results would not include Custom Properties in Chrome due to https://crbug.com/41451306.

And finally, having a requestAnimationFrame forces all animations that run on the Compositor to also re-run on the Main Thread. This because getComputedStyle needs to be able to get the up-to-date value.

Add all those things up, and it becomes clear that requestAnimationFrame is not a feasible solution 🙁

The CSS transitions approachIn 2020, Artem Godin created css-variable-observer which ditched the requestAnimationFrame approach in favor of the CSS Transitions approach. While that library is more performant than the previous attempts it has the big limitation that it only works with (custom) properties that contain <number> values.

This is due to the (clever!) approach to storing all the data into the font-variation-settings property.

The choice for font-variation-settings was made because its syntax is [ <opentype-tag> <number> ]# (with <opentype-tag> being equal to <string>) and it’s a property that is animatable.

🙏 The code for @bramus/style-observer started out as a fork of css-variable-observer. Thanks for your prior work on this, Artem!

Transitioning discretely animatable propertiesJust two days ago, former colleague Jake Archibald shared a StyleObserver experiment of his in the CSS Working Group Issue discussing this. His approach relies on Style Queries and a ResizeObserver to make things work.

In the follow-up discussion, Jake foolishly wrote this:

Huh, could some of the discrete value animation stuff be used to make this work for non-numbers?

Whoa, that was exactly the clue that I needed to go out and experiment, resulting in this Proof of Concept. That POC was then used to build @bramus/style-observer into what it is now 🙂

🙏 Thanks for providing me with the missing piece of the puzzle there, Jake!

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

Introducing @​bramus/style-observer, a MutationObserver for CSS.

It allows you to attach JavaScript callbacks to changes in computed values of CSS properties.

🔗 https://t.co/6XlDe7Ixd0 pic.twitter.com/3lL2rnfiRg

— Bram.us (by @bramus) (@bramusblog) August 30, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

The other day on Mastodon, Ryan wondered how hey can detect support for @starting-style. While in theory you could use @supports at-rule() for this, in practice you can’t because it has no browser support (😭).

Drawing inspiration from my previous post on how to detect support for @property, I came up with a similar method to detect @starting-style support

~

The CodeThe code I landed on is this:

@property --supported {syntax: "<number>";initial-value: 0;inherits: false;}:root {transition: --supported 0s calc(infinity * 1s);}@starting-style {:root {--supported: 1;}} In browsers with support for @starting-style, the value of --supported will be 1. In browsers without support the value is 0. You can use that value with Style Queries, Space Toggles, etc.

~

How it worksThe code works by registering a custom property with an initial-value of 0. The value of that property gets changes to 1 in @starting-style.

To prevent the value from swapping back to 0, it is given a transition delay that lasts for all eternity.

🌟 Shout-out to Schepp who mentioned this “long transition delay”-approach at CSS Day. The idea got stuck in my head and I’m happy I was able to use it shortly after discussing it.

~

DemosEmbedded below is a demo that uses this technique along with Style Queries, my preferred way of responding to custom property values.

See the Pen Detect @starting-style support by Bramus (@bramus) on CodePen.

In browsers with @starting-style and Style Queries support, the body has a green background. In all others the background is red. Go take a peek at the previous post on @property on how to adjust this to use a Space Toggle instead.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

Today on Mastodon, Nils asked how to detect support for @property. While in theory you could use @supports at-rule() for this, in practice you can’t because it has no browser support (😭).

Thankfully, there’s a workaround … by trying to actively use registered custom properties and style things based on the outcome.

~

Feature detecting @property support + Style QueriesThis trick involves registering a Custom Property --supported-sentinel property with a specific initial-value. When then trying to use it inside var() the resulting value will either be that initial value or the fallback you provide. In the following snippet below the value for --supported will be either 1 in browsers with support or 0 in browsers with no support for @property

@property --support-sentinel {syntax: "<number>";initial-value: 1;inherits: false;}:root {--supported: var(--support-sentinel, 0);} Using Style Queries you can then style children of :root based on the value of --supported:

/* No support */body {background: red;}/* Has support */@container style(--supported: 1) {body {background: green;}} The downside of this method is that the browser also needs to support Style Queries, which currently are only supported in Blink/Chromium. I’m sure Jane can remix this to use Type Grinding

DemoSee the Pen CSS @property feature detection (style queries) by Bramus (@bramus) on CodePen.

~

Feature detecting @property support + a Space ToggleA detection with broader support is to use a Space Toggle. One downside is that it requires JavaScript to register the toggler because of differences between browsers.

🤔 What’s a Space Toggle?If you are unfamiliar with the Space Toggle Hack, it’s a hack that relies on a custom property that you toggle between two values: it’s either a space () or initial. The former indicates that it’s ON and the latter that it’s OFF.

--toggler: ; /* = ON */

--toggler: initial; /* = OFF */ You use this property to generate other values: a value for when it’s ON and a value for when it’s OFF.

It relies on CSS eating spaces (e.g. green becomes simply green) and CSS falling back to the fallback value when var() refers to a custom property that contains initial.

Behavior when it’s ON ():

--toggler: ; /* = ON */--value-when-on: var(--toggler) green; /* = ` green` = `green` */--value-when-off: var(--toggler, red); /* = ` ` or `red` = `red` */background: var(--value-when-on, var(--value-when-off)); /* = `green` or ` ` = `green` */ Behavior when it’s OFF (initial):

--toggler: initial; /* = OFF */--value-when-on: var(--toggler) green; /* = `initial green` = `initial` */--value-when-off: var(--toggler, red); /* = `initial` or `red` = `red` */background: var(--value-when-on, var(--value-when-off)); /* = `initial` or `red` = `red` */ This trick also involves registering a custom property but you give it an initial-value of a single space. For reasons detailed further down, I am registering the Custom Property using JavaScript.

CSS.registerProperty({name: '--supported',syntax: '*',initialValue: String.fromCharCode(0x20), // (or just use ' ')inherits: false}); In browsers with support, the value for --supported will be that space. In browsers with no support, the value for --supported will be the guaranteed initial value of initial. Yes, a classic Space Toggle indeed!

body {--bg-if-support: var(--supported) green;--bg-if-no-support: var(--supported, red);background: var(--bg-if-support, var(--bg-if-no-support));} 🤔 Why can’t you rely on registering the Custom Property using CSS?The short answer is that you can register a Custom Property with a space as its initial-value, but that it’s not reliable across browsers. The longer answer is that up until 2023 it wasn’t possible to register a Custom Property without an initial-value. Because the CSS parser eats up spaces it wouldn’t recognize a space as the initial-value, so the following code wouldn’t work.

@property --supported { syntax: '*'; initial-value: ; inherits: false;} With the spaces being eaten, the parser would think the initial-value was missing, and thus it would discard the entire registration. This got fixed at the spec level after I filed a CSSWG Issue for this: the initial-value descriptor is now optional when the syntax is set to *.

While the behavior got updated in Chrome 119 thanks to this commit, you shouldn’t rely on the CSS registration because that would exclude Chrome versions 85-118. Furthermore Safari doesn’t seem to like this CSS registration variant (bug report here) whereas Firefox (with feature flag at the time of writing) OTOH does play nice with it.

Check out this demo: it’s red (wrong) in Safari and green in Chrome 119+ and Firefox Nightly

DemoSee the Pen CSS @property feature detection (Space Toggle by Bramus (@bramus) on CodePen.

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

New post: How to feature detect support for @​property in CSShttps://t.co/SuXh83X719#css #featuredetection #atproperty

— Bram.us (by @bramus) (@bramusblog) July 3, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

An answer to a headscratcher Tyler Gaw (and Chris Coyier) ran into: getting the result of cos(25deg) in CSS.

~

The ProblemOn the CodePen blog, Chris Coyier dug into a blog post by Tyler Gaw – a blog post which I had missed before – in which Tyler wanted to know what the resulting number for cos(25deg) in CSS is.

Take cos(25deg) for example. I know that will return a number between -1 and 1. But what number?

One of the approaches Tyler tried, but then abandoned, was storing that value in a Custom Property and then reading its Computed Value. As Tyler found, this didn’t work:

… using a custom prop like --u: cos(25deg) doesn’t really work either because the custom prop value is stored as cos(25deg)

Checking the Computed Value of --u indeed yields the string cos(25deg) instead of the number you might expect:

```

demo { --u: cos(25deg);}

```

getComputedStyle($0).getPropertyValue("--u");// -> "cos(25deg)" ~

The CauseThe reason why the CSS engine doesn’t fully parse cos(25deg) in Tyler’s demo because the parser doesn’t know what type --u is.

As stated in the Custom Properties specification, a Custom Property computes to:

[The] specified value with variables substituted, or the guaranteed-invalid value.

The further processing of cos(25deg) only happens when that value is used in a declaration that expects a <number>. For example, if you use cos(25deg) as the value for scale – or use var(--u) as the value for scale – getting the computed value for scale will return the resulting number:

```

demo { scale: cos(25deg);}

```

getComputedStyle($0).getPropertyValue("scale");// -> 0.906308 This because the engine tries to parse scale as a its expected type of <number>.

~

The SolutionWinging back to Tyler’s custom property approach, the solution comes in the form of @property, which allows you to register a custom property to be of a certain type.

By telling the parser – using @property – that the custom property’s type is a <number>, the engine will try to parse it as such when getting its computed value.

As per CSS Properties and Values API Level 1 specification:

The computed value of a registered custom property is determined by the syntax of its registration.

💁‍♂️ Note that when this parsing fails, the value can become Invalid at Computed-Value Time (or IACVT for short).

In code, it becomes this:

@property --r { syntax: "<number>"; inherits: false; initial-value: 0;}#demo { --r: cos(25deg);}

getComputedStyle($0).getPropertyValue("--r");// -> 0.906308 So there you have it, Tyler, you no longer need to resort to the JS workaround you came up with 🙂

~

All togetherHere’s a demo that combines all code used in this post:

See the Pen Parsing CSS functions by Bramus (@bramus) on CodePen.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

Me, on stage. Photo by Brecht.Last week I was in Amsterdam to attend CSS Day. At the pre-event organized by Communication and Multimedia Design Amsterdam I gave a talk on Cross-Document View Transitions for use in your Multi-Page Applications (aka: “just websites”).

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave is a full-length talk of a little over 40 minutes.

In 2023 we at Google shipped Same-Document View Transitions for SPAs in Chrome 111. This year we bring you the next big thing for View Transitions: Cross-Document View Transitions for MPAs, activated by a same-origin navigation. Now you no longer need to rearchitect your app to an SPA in order to use View Transitions: clicking a link from one page to another is enough to trigger the transition.

This was a reprise of the talk I did at JSHeroes with the addition of sections covering View Transition Classes and View Transition Active Types, two new features I announced at Google I/O a few weeks ago.

~

SlidesThe slides of my talk are up on slidr.io are embedded below:

Watch the slides

These exported slides don’t contain any recordings of the demos included, but you can follow the link to check them out yourself. For the Same-Document View Transitions demos you will need Chrome 111+. For the Cross-Document View Transitions demos you need Chrome 126+.

~

Recording/VideoThis talk was not recorded. You can check out a recording from the rendition at JSHeroes.

~

Thanks!Thanks to Vasilis, Sanne and Krijn for inviting me to speak at this pre-event and kick off a wonderful three days packed with CSS. It was very delightful to see a lot of you, and it was very inspiring to have Sanne show some of the students their work before the event started. I sure envy those students for having such great lecturers and getting taught The Right Thing™ from the start.

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

https://www.bram.us/wordpress/wp-content/uploads/2024/06/eVJXUTKq6tBFi0H3.mp4Recording of Temani’s demo~

I love this demo by Temani Afif. As you drag the thumb of the range input, the tooltip updates. It’s a combination of @property, counters, anchoring, and scroll-driven animations.

See the Pen CSS-only Custom range slider by Temani Afif (@t_afif) on CodePen.

Cranking it up a noth, Temani also incorporated my approach to a CSS-only scroll velocity detection to make the tooltip jiggle as you drag the thumb across.

See the Pen CSS-only Custom range slider with motion by Temani Afif (@t_afif)on CodePen.

Love it! 😍

View Details

View from the stage, a few minutes before I went up.I’m currently in Prague (Czech Republic) where I’m attending WebExpo. Earlier today I gave a talk on Scroll-Driven Animations with CSS.

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave is a full-length talk of 40 minutes.

A new addition to CSS is “Scroll-driven animations”. With it, you can link animations to the scroll offset of a scroll container using only CSS — no JavaScript needed! In this demo-heavy presentation, Bramus will dig into what this specification has to offer, dissect some of its demos (such as a working Cover Flow implementation), and cover common use-cases.

I have given a similar talk before, but this talk received a major overhaul for this occasion.

~

SlidesThe slides of my talk are up on slidr.io are embedded below:

Watch the slides

~

Recording/VideoThe talk was recorded and will be published later. Once that has happened I’ll update this post to include it here.

~

Thanks!Thanks to the organizers for having me and thanks to the attendees for being there. It was an honor to speak to you at this BEAUTIFUL venue and to be part of the AMAZING line-up full of wonderful people (Ana, Cassie, Harry, Vitaly, Nishu, Nathan, Geri, Claire, Phil, Robin, Krasimir, …).

Some of the speakers (me including) at the afterparty. Selfie by Phil.Also great I got to meet some of you IRL, after only having interacted with you online before (Ana, Cassie, Harry, Vitaly, and Vlad!)

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

Promotional screenshot as used on social media (tweet, toot)~

📣 PSA: Time to update your MPA View Transitions experimentations!The opt-in for Cross-Document View Transitions for MPA is by means of the @​view-transition at-rule in CSS.

@view-transition { navigation: auto;} The meta tag that you might know from before was a temporary thing while the feature was being developed behind a flag.

Cross-Document View Transitions are shipping in Chrome 126.

There’s also two new things that you can use with View Transitions nowadays:

  1. View Transition Types
  2. view-transition-class

See https://developer.chrome.com/blog/view-transitions-update-io24?hl=en for an overview (text + video) of all additions and improvements.

~

Spread the wordFeel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.

📣 PSA: Time to update your MPA View Transitions demos!

The opt-in for Cross-Document View Transitions for MPA is by means of the @​view-transition at-rule in CSS.

The meta tag from before was a temporary thing while the feature was being developed behind a flag. pic.twitter.com/ly29v1Z5wn

— Bramus (@bramus) May 24, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

I’m currently in Cluj-Napoca (Romania) to speak at JSHeroes. Earlier today I gave a talk on Cross-Document View Transitions for use in your Multi-Page Applications (aka: “just websites”).

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave is a full-length talk of a little over 30 minutes.

In 2023 we at Google shipped Same-Document View Transitions for SPAs in Chrome 111. This year we bring you the next big thing for View Transitions: Cross-Document View Transitions for MPAs, activated by a same-origin navigation. Now you no longer need to rearchitect your app to an SPA in order to use View Transitions: clicking a link from one page to another is enough to trigger the transition.

This was a novel talk, re-using part of my talk on SPA View Transitions which I gave at CSS Day in 2023 to introduce the concept of View Transitions, augmented with a novel section on Cross-Document View Transitions which I announced at Google I/O last week.

~

SlidesThe slides of my talk are up on slidr.io are embedded below:

Watch the slides

~

Recording/VideoThe talk was recorded and will be published later. Once that has happened I’ll update this post to include it. In the mean time, you can check out the talk I did at Google I/O 2024 which also covers Cross-Document View Transitions.

~

Thanks!Thanks to the organizers for having me and thanks to the attendees for being there. It was great to have a full room full of you at such a wonderful location. A pity I have to leave early on Saturday, as I would have loved to check out the surroundings. Maybe next year 😉

Me, on stage. Photo by Tejas.It was also great to meet a bunch of you again and also to to see some of you who I’ve only interacted with online before in-person for the first time.

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

For this year’s Google I/O, I recorded the session “Multi-page application View Transitions are here”.

Last year, we debuted Same-Document View Transitions for use in your Single-Page Applications (SPAs), and now we’re taking it to the next level. Get ready for enhanced control over View Transitions to more easily create immersive user experiences. We are also giving you Cross-Document View Transitions which allow you to create seamless navigations in your Multi-Page Applications (MPAs).

In the talk, I cover two main topics:

  1. Cross-Document View Transitions for Multi-Page Applications
  2. View Transitions Improvements (view-transition-class and View Transition Types)

You can find the video embedded at the top of this post. If you’re more into reading, check out the announcement blog post. The documentation up on developer.chrome.com also got an overhaul.

Watch “Multi-page application View Transitions are here” on YouTube →
Read the announcement blog post →
Read the updated documentation (SPA + MPA) →

View Details

I’m proud to announce the release of “Unleash the Power of Scroll-Driven Animations”, a 10-part video course I created that teaches you all there is to learn all about scroll-driven animations with CSS or JavaScript.

With scroll-driven animations you can take an existing CSS or WAAPI animation and animate it on scroll. Because of that, scroll-driven animations inherit all of the benefits these APIs bring. This includes hardware accelerated scroll-driven animations that are not subject to jank when the main thread gets blocked.

📝 Read the announcement post →
📺 Watch the videos on YouTube →
🧑‍🔬 Check out the demos and tools →
🧑‍💻 Get the debugger extension →

View Details

Me, at CSS Day 2022, talking about Specificity (and the Cascade)Whenever a post about Specificity in CSS – and by extension the Cascade itself – gets published I get very excited as it’s a core concept of the language that everyone should know. The more articles on this, the better!

However, I also sometimes raise one of my eyebrows as sometimes I, unfortunately, encounter something that’s just outright wrong.

To remove some of the confusion, here’s a list of misconceptions about Specificity in CSS …

~

Table of Contents* Misconception 1: Specificity is a decimal score

  • Misconception 2: Using the style attribute adds Specificity
  • Misconception 3: Using !important adds Specificity
  • Learn More

~

Misconception 1: Specificity is a decimal scoreSome, typically older, articles out there express Specificity as a decimal score, or mention that a thing like a class selector “adds 10 points”. This is inaccurate as it would imply that 11 element selectors with their “1 point each” would beat 1 class selector that only has “10 points”.

Instead, Specificity is a triple (or a triad) that has three components: A, B, and C. The values for A, B, and C depend on what type of selector you use.

  • A = id-like specificity
  • B = class-like specificity
  • C = element-like specificity

It is often represented using the (A,B,C) notation. For example: (1,0,2). The alternative A-B-C notation is also commonly used.

🤓 I wrote a JavaScript library that can calculate the specificity of a Selector: @bramus/specificity. It powers this interactive specificity calculator.

Specificities are compared by comparing the three components in order: the specificity with a larger A value is more specific; if the two A values are tied, then the specificity with a larger B value is more specific; if the two B values are also tied, then the specificity with a larger C value is more specific; if all the values are tied, the two specificities are equal.

In code, it looks like this:

const compare = (s1, s2) => { if (s1.a === s2.a) { if (s1.b === s2.b) { return s1.c - s2.c; } return s1.b - s2.b; } return s1.a - s2.a;}; For example, (1,0,0) is a higher specificity than (0,10,3) because the A value in (1,0,0) (which is 1) is greater than the A value from (0,10,3) (which is 0).

Also see this excerpt from my CSS Day 2022 talk about the cascade:

Excerpt from my CSS Day talk on the Cascade, covering how to write and compare Specificity💡 While you could represent the triad as a decimal by adding enough leading zeroes, it comes with its own set of challenges: the number you end up with is hard to read/parse + you would need to add “enough” zeroes to get a correctly sortable result.

My advice here remains this: don’t.

Where the confusion comes from:Back in CSS Selectors 3, the spec mentioned that you could concatenate the numbers to get the specificity.

Concatenating the three numbers a-b-c (in a number system with a large base) gives the specificity.

While it is caveated with “in a number system with a large base” the clause got ignored, most likely because the spec showed examples in base 10 – e.g. #s12:not(FOO) /* a=1 b=0 c=1 -> specificity = 101 */.

Almost 15 years later and we are still stuck with it: some writers keep representing Specificity in base 10 because it’s “simpler to explain”. While that might be true, it’s teaching people the wrong thing. Having them unlearn something later on is much much harder to do.

~

Misconception 2: Using the style attribute adds SpecificityI often read that using the style attribute “adds 1000 points of specificity”. This is not correct as evaluation of the style attribute is an earlier step of the cascade – it has nothing to do with Specificity at all.

The various steps of the cascade (as per CSS Cascade 5), with style attribute highlightedWhere the confusion comes from:Back in the CSS2 days (in 2011) this was actually the case. In that spec, Specificity was quadruple (A,B,C,D), with the style attribute being the A component.

Since CSS3, this is no longer the case, though.

~

Misconception 3: Using !important adds SpecificityI often read that adding !important to a declaration “adds 10000 points of specificity”. This is not correct as using !important puts the declaration in a different origin. By using !important in your author styles, the declaration moves from the Normal Author Declarations origin to the Important Author Declarations origin.

The various origins (as per CSS Cascade 5) with normal author declarations and important author declarations highlightedOrigins and importance is the first criterion of the cascade that is checked. Specificity comes much much later.

Where the confusion comes from:I couldn’t find any hard traces in the specifications, but my guess this was something someone came up back in the day when the Specificity directly followed the Origins criterion. That simplification did make sense back then but nowadays it no longer does as it skips over three extra criterions of the cascade that sit in between Origins+Importance and Specificity: Context, Element attached styles, and Layers.

~

Learn moreTo learn more about Specificity and the Cascade, I’d recommend you to watch the talk I gave a CSS Day 2022 that digs into all there is to know about it.

On the dedicated post here on bram.us you can also grab the slides.

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

On April 25, 2024 I gave a talk at CSS Café (event link) on how I combined two of my favorite web features and built a “Scroll-Driven View Transition”.

~

Table of Contents* The Talk * Slides * Recording/Video * Thanks!

~

The TalkThe talk I gave is titled “If View Transitions and Scroll-Driven Animations had a baby …”

With View Transitions, you can create immersive native-like experiences that have smooth and seamless transitions between views of your website. With Scroll-Driven Animations you can drive animations by the act of scrolling. What if you wanted to combine both to create a “Scroll-Driven View Transition”? Could that ever work? And if so, how does it work?

The talk is essentially a slide-ified version of this thread on Twitter / Mastodon which revolves around the following demo.

https://www.bram.us/wordpress/wp-content/uploads/2024/04/scroll-driven-view-transitions-001-050p.mp4Recording of the built demo.
It features a View Transition that is controlled by the act of scrolling.You might recognize the UI here, as it’s a follow-up to the View Transitions poster-child demo by Maxi Ferreira.

The talk doesn’t just dissect the demo, but also teaches you Same-Document View Transitions (referencing Maxi’s demo) and Scroll-Driven Animations + its ranges. I also teased my upcoming video course on Scroll-Driven Animations along with that.

~

SlidesThe slides of my talk are available on slidr.io and are also embedded below:

Because this talk is animation heavy and the online slides don’t support animations or video, you’re most likely better off watching the recording.

~

Recording/VideoThe talk was recorded. You can watch the video on YouTube or right here:

If you’re already familiar with the ins and outs of View Transitions, you can skip to the 27:30 mark of the video.

~

Thanks!Thanks to organizers Schepp and Nils for having me for the third time at CSS Café. Thanks to all attendees for following along and asking some nice questions afterwards during the Q&A.

It was also great to see many new faces and also to see some of you who I had, up until then, only interacted with through chat for the first time.

~

💁‍♂️ If you are a conference or meetup organiser, don't hesitate to contact me to come speak at your event.

View Details

I’ve written about light-dark() before here on bram.us, and last month I also wrote an article for web.dev about it.

The article takes a bit of a different approach, so it’s still worth a read even when you’ve seen my previous one before.

System colors have the ability to react to the current used color-scheme value. The light-dark() function exposes the same capability to authors.

I’m especially fond of this demo that I built. The page responds to your Operating System’s light/dark setting but also allows overriding light/dark mode by changing the value of the color-scheme to only light or only dark.

See the Pen
Color switching with light-dark() and color-scheme by web.dev (@web-dot-dev)
on CodePen.

CSS color-scheme-dependent colors with light-dark()

View Details

https://www.bram.us/wordpress/wp-content/uploads/2024/04/chrome-dark-mode-toggle-extension-teaser.mp4Recording of a POC Browser ExtensionIf you’ve been following me for a while you most likely know that I think that Dark Mode Toggle Buttons should be a browser feature: Let the user decide which color-scheme they want on a per-site basis and store that.

Thanks to the Web Preferences API, I was able to build a proof of concept extension.

~

The Web Preferences APISince I wrote that Dark Mode Toggle Buttons should be a browser feature almost two years ago, Luke Warlow has been working on the Web Preferences API. The Web Preferences API offers a way for sites to override the value for a given user preference (e.g. color-scheme preference) in a way that fully integrates with existing Web APIs.

Example of the Web Preferences API overriding the reported color scheme preference.Thanks to this API, you can ask the website to override the reported value of prefers-color-scheme to light or dark. This in contrast to existing approaches where you toggle a class or data-attribute on :root and then need to duplicate your dark styles in your CSS – an approach I have detailed here.

The API is experimental and only available in Chrome Canary behind the “Experimental Web Platform Features” feature flag. While the API does its thing, Chrome itself doesn’t do anything with the changes yet. If you override a value on a site, the override is not persisted in Chrome.

~

An extension to bridge the gapTo get closer to that “Browser Dark Mode Toggle” I’ve been wanting, I’ve built a POC extension that can override and persist the prefers-color-scheme value. The extension adds a button to Chrome that allows you to cycle through systemdarklight on a per-origin basis.

Recording of the extension in action.Overrides get persisted in your Chrome profile. So if you re-open a website, or switch from your one Chrome to your other, the chosen override gets applied. That way your OS can remain in Light Mode, while a certain website can always be in Dark Mode (or vice versa).

Furthermore because the extension uses the Web Preferences API, it can also respond to changes invoked by the website. See 2nd part of the recording where the extension responds to Luke’s site changing the preference. I love it how both the browser (extension) and website nicely work together here.

You can install extension from the Chrome Web Store. I have a README and FAQ available on GitHub, where you can also file an issue should you run into anything.

Get the extension ↗Check README + FAQ ↗~

Not the final solutionThis extension is not a final solution but more of a POC to show how a Dark Mode Toggle in the browser could work. The integration for example isn’t everything right now, an implementation built right into Chrome would be more seamless and better integrated.

Maybe these dark/light preferences could have a place here, along with the other site-specific settings?For example, because with this extension you get a FART (Flash of inAccurate coloR Theme) as it relies on injecting scripts. If this override would ever become a proper Chrome feature, you would not have the FART.

So consider all this more of a POC to gauge user interest, rater than a 100% polished thing 😉

To express your interest in this user feature, go vote on one of the polls on social media: Twitter or Mastodon

~

Spread the wordThis post originally was a thread on Twitter and on Mastodon. Feel free to repost those threads to give them more reach.

🌞 ~vs~ 🌑

If you’ve been following me for a while here you most likely know that I think that Dark Mode Toggle Buttons should be a browser feature:

Let the user decide which color-scheme they want on a per-site basis and store that. https://t.co/jIOXqdJNWf

— Bramus (@bramus) April 12, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

I’m running a poll on socials (Twitter, Mastodon) asking whether DevTools should flag the IACVT state of a declaration or not.

Besides the obvious “Yes” and “No”, I also added a “What’s IACVT?” option.

To my surprise, many respondents chose that last answer I provided … so here’s a post that tells you all about “Invalid At Computed Value Time” or IACVT for short.

~

Table of Contents* Invalid Declarations

  • CSS Value Processing
    • Example: From 1.2em to 14px in 6 steps
  • Custom Properties
  • IACVT: Valid Declarations with Invalid Values
  • The Consequences of IACVT
  • Summary
  • Spread the word

~

Invalid DeclarationsIn CSS all (non-custom) properties only accept values of a certain type. For example, the background-color property is defined to only accept a <color> type of value.

Namebackground-colorValue<color>InitialtransparentApplies toall elementsInheritednoPercentagesN/AComputed valuecomputed colorCanonical orderper grammarAnimation typeby computed valueThe definition also answers questions such as “what is the initial value”, “does this property inherit”, “how does it behave when used in an animation”, etc.

If you try to put something like 20px – which is a <length> – into the background-color property, the declaration gets discarded because it has an invalid property value.

body {color: blue;~~color: 20px;~~ /* ❌ Invalid Property Value */} These invalid declarations get discarded very early in the CSS Value Processing process. When collecting the Declared Values, User-Agents (= spec lingo for browsers) already filter out syntactically invalid declarations:

In order to find the declared values, implementations must first identify all declarations that apply to each element. A declaration applies to an element if:

  • It belongs to a style sheet that currently applies to this document.
  • It is not qualified by a conditional rule with a false condition.
  • It belongs to a style rule whose selector matches the element. (Taking scoping into account, if necessary.)
  • It is syntactically valid: the declaration’s property is a known property name, and the declaration’s value matches the syntax for that property.

In case you were wondering: the <color> type itself is split up into several sub-types, and therefore accepts a multitude of possible values. It is defined to accept hex colors, named colors, the currentcolor keyword, color functions (such as rgb() and oklab()), light-dark(), etc.

~

CSS Value ProcessingBetween you declaring a value in your stylesheet and an element knowing which value to actually use, there’s a whole multi-step calculation that kicks into action. This process is known as CSS Value Processing.

  1. First, all the declared values applied to an element are collected, for each property on each element. There may be zero or many declared values applied to the element.
  2. Cascading yields the cascaded value. There is at most one cascaded value per property per element.
  3. Defaulting yields the specified value. Every element has exactly one specified value per property.
  4. Resolving value dependencies yields the computed value. Every element has exactly one computed value per property.
  5. Formatting the document yields the used value. An element only has a used value for a given property if that property applies to the element.
  6. Finally, the used value is transformed to the actual value based on constraints of the display environment. As with the used value, there may or may not be an actual value for a given property on an element.

~

Example: From 1.2em to 14px in 6 stepsFor example, take this rule:

p {font-size: 16px;font-size: 1.2em;} The Value Processing process works like this:

Declared ValueThere are two declared values here for font-size, both competing with each other.Cascaded ValueAfter running the cascade, the winning declaration is font-size: 1.2em; so the cascaded value is 1.2em.Specified ValueBecause there is no defaulting at play the specified value is also 1.2emComputed ValueCalculating the computed value resolves the specified value as far as possible without laying out the document or performing other expensive operations. Generally speaking, computing a relative value absolutizes it so the 1.2em computes to 14.1px (for example).Used ValueBecause there are no remaining calculations to be done, this 14.1px also becomes the used value.Actual ValueDepending on the user agent / hardware – e.g. on a device that can only draw full pixels – this 14.1px might become 14px which is the actual value.👨‍🏫 The spec has more examples of how certain values get processed.

~

Custom PropertiesCustom Properties is CSS’s answer to having variables in the language. These are not just static tokens that you declare once and can never change, but these are actual properties that take part in the cascade. You can change their value on specific elements, have them respond to media queries, etc.

To use them in a declaration, use the var() function.

:root { --color: green; }p { background-color: var(--color); } These var() functions are substituted at computed-value time.

Declared Valuevar(--color)Cascaded Valuevar(--color)Specified Valuevar(--color)Computed Valuegreenrgb(0, 128, 0)Used Valuergb(0, 128, 0)Actual Valuergb(0, 128, 0)In this case here, you end up with a green background-color, as you might have expected 🙂

Note that green resolves to rgb(0, 128, 0) as per css-color-4 spec:

If the sRGB color was explicitly specified by the author as a named color […] the computed and used value is the corresponding sRGB color, paired with the specified alpha channel (after clamping to [0, 1]) and defaulting to opaque if unspecified).

~

IACVT: Valid Declarations with Invalid ValuesWhile a declaration in the form of property: var(--varname) is valid in itself, you can end up with a declaration that makes no sense when the var() function gets substituted.

Consider the following example from the spec:

:root { --not-a-color: 20px; }p { background-color: red; }p { background-color: var(--not-a-color); } When processing the values, you end up with this:

Declared Valuevar(--not-a-color)Cascaded Valuevar(--not-a-color)Specified Valuevar(--not-a-color)Computed Value16px ❌Used Value??Actual Value??The 16px you see at computed-value time there is problematic, because 16px is not a valid value for background-color which only accepts <color>s.

This declaration is therefore considered Invalid at Computed-Value Time, or IACVT for short:

A declaration can be invalid at computed-value time if it contains a var() that references a custom property with the guaranteed-invalid value […] or if it uses a valid custom property, but the property value, after substituting its var() functions, is invalid

☝️ For completeness, the guaranteed-invalid value you see there is initial. But that’s food for a different blog post …

~

The Consequences of IACVTWhen a declaration is considered IACVT is does not get thrown away nor does it fall back to other declarations as those have already been thrown away at Cascaded-Value time.

Instead, the invalid value gets replaced by a different value. What exactly happens depends on the property’s type.

  • If the property is a non-registered custom property or a registered custom property with universal syntax it gets replaced by the guaranteed-invalid value.
  • In all other cases the value becomes unset, whose outcome depends on whether the property is allowed to inherit or not.
    • If the property is allowed to inherit, it behaves as inherit, so it inherits the computed value from its parent.
    • If the property is not allowed to inherit, it behaves as initial, falling back to the initial value.

So winging back to this example:

:root { --not-a-color: 20px; }p { background-color: red; }p { background-color: var(--not-a-color); } Things play out like this:

  1. The Cascaded Value is var(--not-a-color). The background-color: red; declaration gets thrown away because it is of no use.
  2. The background-color: var(--not-a-color); gets marked as IACVT at Computed-Value time.
  3. var(--not-a-color) gets replaced by unset because it was IACVT
  4. unset behaves as initial because background-color does not inherit
  5. The computed value becomes transparent as that is the initial value for background-color.

Here’s a demo that shows this. The background is not red but transparent.

See the Pen CSS IACVT Example by Bramus (@bramus) on CodePen.

💡 If you would have authored background-color: 20px directly (i.e. without the use of Custom Properties), then that declaration would have simply been discarded due to being invalid, and you would have ended up with a red background.

~

SummaryCSS has a process in place called Value Processing to determine the actual value to apply to an element. In this process, invalid declarations like color: 20px get filtered out upfront.

Declarations with custom properties like color: var(--not-a-color) can initially make it through, but can get kicked out in case the substituted value does not match the type the property expects.

Because this invalidation happens at Computed-Value time, the declaration is flagged as “Invalid at Computed-Value Time” (IACVT).

When IACVT occurs, the invalid value gets replaced by either the guaranteed-invalid value or unset, depending on what property is targeted.

~

Spread the wordTo help spread the contents of this post, feel free to retweet the announcements made on social media:

For those who answered “What’s IACVT?” I wrote a blog post for you: https://t.co/vbMkPxExPJ

— Bramus (@bramus) February 26, 2024

~

🔥 Like what you see? Want to stay in the loop? Here's how:

  • Follow @bramus on Twitter
  • Follow @bramus on Mastodon
  • Follow @bramusblog on Twitter
  • Follow bram.us using RSS

View Details

Screenshot of the demo featured in the article. In Chrome 122 the backdrop is light purple because it can access the custom property from the dialog element.If you ever struggled with ::backdrop not having access to custom properties, here’s some good news:

As of Chrome 122 – and also in Firefox 120 and soon Safari 17.4 – ::backdrop is a tree-abiding pseudo-element, meaning that it inherits any inheritable properties from its originating element.

Go check out the article I wrote for developer.chrome.com for all details + a demo.

Changes to CSS ::backdrop inheritance →

View Details

Nuanced piece by Tero Piirainen on where Tailwind took a good practice, twisted it around, and took it for a run.

Use it or don’t, love it or hate it, but this last sentence of the article hits close to home:

Learn to write clean HTML and CSS and stay relevant for years to come.

Frameworks come and go. The foundations are here to stay.

Tailwind marketing and misinformation engine →

View Details

Great article by Jake, packed with demos on how to handle changes in aspect ratios between the old and new snapshots in View Transitions.

View transitions: Handling aspect ratio changes →

View Details

Jacob Rask, Volvo Cars Online Engineering, at the end of 2022 explaining why they are moving away from CSS-in-JS to CSS for the VVC UI project:

By utilising CSS-in-JS and colocating the styles with the component we avoid including unused CSS on our pages; a component’s styles will only be included on a page when it’s needed and if a feature is removed from a product, its styles will be deleted with it. At the same time, component based styles with CSS-in-JS lack some of the expressiveness of CSS selectors and assumptions you can make when writing plain CSS. Further, every component needs to start from scratch, resetting styles in a way that works across all browsers.

CSS-in-JS inlined in