CSS @​supports rules to target only Firefox / Safari / Chromium

Yesterday I took some time to rework my Houdini-powered CSS Gradient Border Animation Demo to include a fallback for non-Houdini browsers.

The plan of attack was pretty straightforward:

  • Manual frame-by-frame animations for non-Houdini browsers
  • Automagic Houdini-powered animations for browser with @property support

Only problem with that approach is that there’s currently no way to use @supports to detect whether a browser supports Houdini’s @property or not, so I needed to get creative with @supports

~

🎩 Houdini, ain't that a magician?

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

~

Table of Contents

  1. What I want
  2. Getting creative with @supports
  3. The @supports rules
  4. Combined Demo
  5. In Closing

~

# What I want

Ideally I want to check for support for @property using something like this:

/* Syntax Proposal */
@supports (@property) {
  …
}

/* Syntax Proposal, Expanded to check for certain descriptor support */
@supports (@property { syntax: "<angle>" }) {
  …
}

/* Alternative Syntax Proposal */
@supports descriptor(@property, syntax: "<angle>") {
  …
}

Unfortunately this is currently not possible and considered a shortcoming of CSS @supports. The issue is being discussed in CSSWG Issue 2463.

~

# Getting creative with @supports

This blogpost was updated to be able to target Mobile Safari and Desktop Safari separately.

💁‍♂️ Note that I’ll be using browser names here (e.g. Firefox), even though the statements apply to their underlying rendering engines (e.g. Gecko). And when talking about “Chromium” I mean all browsers that are based on it (e.g. Google Chrome, Microsoft Edge, Brave, etc.)

As Chromium (Blink) currently is the only browser that supports @property, we need to create a @supports rule that targets Chromium only. While at it let’s also create @supports rules to target only Safari (WebKit), Firefox (Gecko), or a combination thereof.

Looking at what each browser uniquely supports, these four rules can be used as a starting point:

  1. Only Firefox supports -moz-appearance: none
  2. Only Safari supports the :nth-child(An+B [of S]?) selector. Using selector() function in @supports we can check whether the browser supports it or not. Only Desktop Safari understands the combination of both though.
  3. Only MobileSafari (Safari on iOS) supports -webkit-touch-callout: none
  4. Both Chromium and Firefox support contain: paint

👉 By combining/excluding these rules you can target a set of browsers, or only one specific one.

~

# The @supports rules

🚨 Note that the following @supports rules are a hacky workaround which reeks a lot like browser sniffing. They’re very fragile as rendering engines may add support for those properties/selectors over time. Things can — and will — break in the future. Be ye warned.

# Targeting Firefox Only

Firefox is the only browser that supports -moz-appearance: none

/* Firefox Only */
@supports (-moz-appearance: none) {
  …
}

# Targeting Not-Firefox (e.g. Chromium + Safari)

By negating the selector only Firefox supports you can target Chromium + Safari:

/* Chromium + Safari */
@supports (not (-moz-appearance: none)) {
  …
}

# Targeting Safari Only

Safari is the only browser that supports the complex :nth-child(An+B [of S]?) selector — which itself allows you to create a :nth-of-class-like selector — so you can rely on that:

/* Desktop Safari Only */
@supports selector(:nth-child(1 of x)) {
  …
}

Even though the relevant WebKit bug is marked as resolved, my testing showed that Mobile Safari does not understand selector(), so we need something extra to also target Mobile Safari.

/* Mobile Safari Only */
@supports (-webkit-touch-callout: none) {
  …
}

By combining both these conditions we can target both Safaris.

/* Desktop & Mobile Safari Only */
@supports (-webkit-touch-callout: none) or (selector(:nth-child(1 of x))) {
  …
}

Note that the extra parens around selector() are crucial here. When omitted MobileSafari can’t interpret the whole rule.

# Targeting Not-Safari (e.g. Chromium + Firefox)

You could negate the selector above to target Chromium and Firefox, but instead I chose to check for support for contain: paint which both browsers support:

/* Chromium + Firefox */
@supports (contain: paint) {
  …
}

The reason why I choose this selector becomes clear in the next part 😉

# Targeting Chromium Only

To target Chromium only you can select both Chromium and Firefox (using @supports (contain: paint)), and then exclude Firefox from there (using @supports (not (-moz-appearance: none))).

Combined you get this @supports rule:

/* Chromium Only */
@supports (contain: paint) and (not (-moz-appearance: none)) {
  …
}

# Targeting Not-Chromium (e.g. Safari + Firefox)

As @supports understands OR-logic, you can combine the rules for both Safaris and Firefox into one:

/**/
@supports (-webkit-touch-callout: none) or (selector(:nth-child(1 of x))) or (-moz-appearance: none) {
  …
}

~

# Combined Demo

Below is a combined demo. The background-color of the body should be:

  • Chromium: blue
  • Firefox: lime
  • Desktop Safari: red
  • Mobile Safari: orange

See the Pen CSS @support rules to target only Firefox / Safari / Chromium by Bramus (@bramus) on CodePen.

Additionally you can also check this combined image (“cheat sheet”):

~

# In Closing

I’m glad I could flesh things out and create @supports rules to target the three major rendering engines. Let’s hope we can drop these workarounds soon, when CSSWG Issue 2463 gets resolved (and implemented) 🙂

~

To help spread the contents of this post, feel free to retweet its announcement tweet:

Follow @bramus (= me, the author) and/or @bramusblog (= the feed of this blog) on Twitter to stay-up-to date with future posts. RSS also available.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

TablesNG — Improvements to <table> rendering in Chromium

Shipped with Chromium 91 is TablesNG, a under-the-hood rewrite regarding tables.

The old table implementation — from WebKit before — was very old, and limited further development. The rewrite that landed emphasizes correctness, fixing 72 bugs in one sweep.

From the list of fixed bugs mentioned in the developer notes, these stand out to me.

~

Table of Contents

  1. Subpixel geometry
  2. <TD> supports orthogonal writing modes
  3. visibility: collapse; for table columns
  4. Sections/rows can have position: that is not static
  5. In Closing

~

# Subpixel geometry

With Subpixel Geometry three cells in a 100px table will now be given a width of 33.333333px each. Before the first two cells would get a width of 33px, and the third one would get a width of 34px.

~

# <TD> supports orthogonal writing modes

This allows us to have rotated table headers in tables, without needing to resort to extra spans and CSS rotations:

See the Pen TablesNG — TD supports orthogonal writing modes by Bramus (@bramus) on CodePen.

The code looks like this:

thead th:not(:first-child) {
	writing-mode: vertical-lr;    /* Switch to vertical writing mode, rendering the label text at 90 degrees */
	text-align: right;            /* Align labels to visual bottom edge */
	padding-top: 1em;             /* Add padding to visual top */
}

If you’re not using Chromium 91+, you can check this visual reference:

To me the labels are visually rotated in the wrong direction though: they are rotated 90 degrees to the right. To make it visually more pleasing (to me) I want them to be rotated 90 degrees tot eh left (e.g. -90 degrees). Changing from vertical-lr to vertical-rl has no effect on this, but thankfully we can shake some more CSS Magic out of our sleeves using scale(-1):

thead th:not(:first-child) {
	writing-mode: vertical-lr;     /* Switch to vertical writing mode, rendering the label text at 90 degrees */
	transform: scale(-1);          /* Flip it 180 degrees */
	text-align: left;              /* Align labels to visual bottom edge */
	padding-bottom: 1em;           /* Add padding to visual top */
}

See the Pen TablesNG — TD supports orthogonal writing modes (rotated) by Bramus (@bramus) on CodePen.

~

# visibility: collapse; for table columns

This allows us to hide entire columns by setting visibility: collapse; on a column in a <colgroup>

col.hidden {
	visibility: collapse;
}

See the Pen TablesNG — visibility: collapse; for table columns by Bramus (@bramus) on CodePen.

If you’re not using Chromium 91+, you can check this visual reference:

~

# Sections/rows can have position: that is not static

This one is a huge addition, as it — finally — allows us to set position: sticky on table headers!

thead {
  position: sticky;
  top: 0;
}

Yes, that piece of code will work as expected 🥳

Over at CSS-Tricks, Chris Coyier has done a write-up + demo:

See the Pen Sticky Table Headers and Footers by Chris Coyier (@chriscoyier) on CodePen.

You can also set position: sticky; on individual <th> elements instead of the <thead> if you want.

~

# In Closing

While these changes are very welcome, there unfortunately are some compatibility issues: Safari still uses the “old” tables rendering engine and drags every other browser down with it that way. Firefox led the way before regarding table rendering, and can quite keep up with Chromium’s TablesNG.

~

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

The CSS Podcast

I don’t follow many podcasts to be honest. I can count the number of followed ones on one hand: exactly 5. As I tend to listen to music throughout the day, 3 out of those 5 are music-related podcasts, featuring mixes by DJs — Great way to discover new music (still miss you, BeyondJazz!).

When it comes to non-music related podcasts, I’m actively listening to The CSS Podcast hosted by Una Kravets and Adam Argyle

Cascading Style Sheets (CSS) is the web’s core styling language. For web developers, It’s one of the quickest technologies to get started with, but one of the hardest to master. Follow Una Kravets and Adam Argyle, Developer Advocates from Google, who gleefully breakdown complex aspects of CSS into digestible episodes covering everything from accessibility to z-index.

I chimed in at the end of its first season and have been listening every since, very good content. It’s quite fast-paced, so you’ll need to direct your full attention to it though.

The CSS Podcast →
The CSS Podcast Season 2, Episode 16: Scroll-Timeline →

~

The latest episode of the show is about CSS Scroll-Timeline, something I’ve been writing, tweeting, and speaking about a lot ever since I discovered it mid-January (after seeing a tweet by Adam on it).

🤩 I’m very excited about this episode, especially since my talk covering Scroll-Timeline was picked up and got featured in it.

Here are the direct links to the visualisations mentioned in the episode:

💁‍♂️ More info, context, and a ton demos can be found in my talk on the subject.

How to optimize ORDER BY RANDOM()

Doing a ORDER BY RAND() in SQL is bad. Very bad. As Tobias Petry details (and Bernard Grymonpon always used to tell at local meetups):

Ordering records in a random order involves these operations:

  1. Load all rows into memory matching your conditions
  2. Assign a random value RANDOM() to each row in the database
  3. Sort all the rows according to this random value
  4. Retain only the desired number of records from all sorted records

His solution is to pre-add randomness to each record, in an extra column. For it he uses a the Geometric Datatype POINT type. In Postgres he then uses the following query that orders the records by distance measured against a new random point.

SELECT * FROM repositories ORDER BY randomness <-> point(0.753,0.294) LIMIT 3;

~

In MySQL you also have a POINT class (ever since MySQL 5.7.6) that you can use. However I don’t really see how that would work there, as you’d need to calculate the distance for each record using a call to ST_Distance:

SET @randomx = RAND();
SET @randomy = RAND();
SELECT *, ST_Distance(POINT(@randomx, @randomy), randomness) AS distance FROM repositories ORDER BY distance DESC LIMIT 0,3;

💁‍♂️ Using EXPLAIN on the query above verifies it doesn’t use an index, and thus goes over all records.

What I do see working instead, is use of a single float value to hold pre-randomness:

-- Add column + index
ALTER TABLE `repositories` ADD `randomness` FLOAT(17,16) UNSIGNED NOT NULL AFTER `randomness`;
ALTER TABLE `repositories` ADD INDEX(`randomness`);

-- Update existing records. New records should have this number pre-generated before inserting
UPDATE `repositories` SET randomness = RAND() WHERE 1;

With that column in place, you could then do something like this:

SET @randomnumber = RAND(); -- This number would typically be generated by your PHP code, and then be injected as a query param
SELECT * FROM repositories WHERE randomness < @randomnumber ORDER BY randomness DESC 0,3;

Unlike the query using POINT(), this last query will leverage the index created on the randomness column 🙂

~

How to optimize ORDER BY RANDOM()

Via Freek

How to Clean up Async Effects in React

Dmitri Pavlutin walks us through properly cleaning up side-effects in React:

From time to time you might have difficulties at the intersection of component lifecycle (initial render, mount, update, unmount) and the side-effect lifecycle (start, in progress, complete).

Tackled are fetch requests, timers like setTimeout(), debounce or throttle functions, etc.

With the techniques applied, you should no longer see warnings like the one below:

Warning: Can't perform a React state update on an unmounted component.

How to Clean up Async Effects in React →

Chrome 92 — What’s New In DevTools

New in DevTools that ship with Chrome 92 (selection):

What’s New In DevTools (Chrome 92) →

Viewport Unit Based Typography vs. Safari

font-size-vw-tamed

A common thing to do regarding font-sizing is to use Viewport Unit Based Typography, nowadays often combined with CSS min() or clamp():

:root {
  font-size: min(calc(1em + 1vw), 4em);
}

However, as Sara Soueidan details, Safari doesn’t co-operate here:

In Safari on macOS, the fluid text wasn’t really fluid—resizing the viewport did nothing to the font size, even though the latter is supposed to respond to the change in viewport width.

It’s a bug, slated to be fixed in the next version of Safari (Safari TP already has the fix). In the meantime there’s an easy workaround we can use.

More details + demo on Sara’s blog.

Working around the viewport-based fluid typography bug in Safari →

Re-reading that Viewport Unit Based Typography post from 2016 I now see that it also mentions that Safari doesn’t play nice with it. Let this underline the importance of filing bugs: because Sara filed a bug the Safari team came to know about the bug and fixed it (very fast too).