Three important things you should know about CSS :is()

Back in 2019 I shared how the CSS :is() selector will simplify things when writing CSS. What I didn’t know back then, and only have learnt quite recently, are these three important facts about CSS :is():

  1. The selector list of :is() is forgiving
  2. The specificity of :is() is that of its most specific argument
  3. :is() does not work with pseudo-element selectors (for now)

Let’s take look at what that means.


# 1. The selector list of :is() is forgiving

What if you include a selector that’s pure gibberish inside :is()? Will the rule-set be declared invalid or what?

p:is(.foo, #bar, $css:rocks) {
  color: hotpink;

Thankfully :is() is very forgiving here: the $css:rocks part — which in itself is an invalid CSS selector — will simply be ignored, while keeping the rest of the selector list in place.. So using the snippet above, both and p#bar will be colored hotpink. Yay!

Should you try this without :is(), the whole rule-set would become invalid. In the snippet below, none of the paragraphs will be hotpink due to that faulty $css:rocks selector invalidating the whole selector list.

p {
  font-family: sans-serif;
}, p#bar, p$css:rocks { /* ❌ This whole rule-set is declared invalid */
  color: hotpink;

Note that the paragraphs will have font-family: sans-serif applied, as it’s only the invalid rule-set that ends up being ignored.

🔮 In the near future this latter behavior will no longer be the case as the CSSWG intends to modify these rules such that an invalid selector will simply be ignored rather than invalidating the whole selector list. Relevant CSS WG Issue: 3264


# 2. The specificity of :is() is that of its most specific argument

Take the code below. What color will have?

p:is(.foo, #bar, $this:invalid) {
  color: hotpink;
} {
  color: lime;

I won’t be lime but hotpink! This because when calculating the specificity, the specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument.

  • has a specificity of (0,1,1)
  • p:is(.foo, #bar) has a specificity of (1,0,1)

As p:is(.foo, #bar) has a higher specificity, it will “win” here.

☝️ The :not() and :has() pseudo-classes also have their specificity calculated this way.

☝️ If you don’t want to be affected by this, you can use :where() instead of :is(). It works in the same way :is() does, but will always have a specificity of 0. You can cleverly wrap this around other selectors to undo their specificity. Think of :where(:not(…)) for example.

😬 Although I wouldn’t recommend it, you could perfectly do something like :is(#bump#up#the#spe#ci#fi#city#yo, .foo) to override selectors more specific than .foo


# 3. :is() does not work with pseudo-element selectors (for now)

If you read up on the definition of :is() you’ll read that it accepts a “Selector List” which is a comma-separated list of simple, compound, or complex selectors.

When looking up simple selectors, there’s an interesting thing to note:

A type selector, universal selector, attribute selector, class selector, ID selector, or pseudo-class is a simple selector.

Do you see it? Here: pseudo-element selectors are not included in this list. As a result, :is() does not play nice with pseudo-element selectors such as ::before, ::after, ….

🔮 In the future this will become possible though, but not just yet. Relevant CSSWG Issue: 2284


Knowing these three facts about :is() will surely help you understand it better and make using it more fun!

See the Pen The CSS :is() pseudo-class. What color will .foo have? by Bramus (@bramus) on CodePen.

If you understood well, the Pen above should hold no secrets to you anymore 🙂


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.

HSL: a color format for humans

Over at the Cloud Four blog, Paul Hebert digs into HSL colors

Colors on the web are confusing — but they don’t have to be! The HSL format makes it easy for humans and computers to work with color.

Included are some nice CodePen-hosted Color Pickers, such as this one:

See the Pen HSLA Color Picker using Custom Props by Paul Hebert (@phebert) on CodePen.

HSL: a color format for humans →

☝️ Remember that nowadays you can drop the Functional Color Notation (e.g. hsla(180, 50%, 50%, 100%)) and use the Space-Separated Color Notation (e.g. hsla(180 50% 50% / 100%)) instead. It’s supported in all major browsers.

Easily write Twitter threads with ThreadStart

Whenever I write a Twitter Thread I carefully lay out my planned tweets in advance in a text editor. After manually adjusting my sentences to have tweetable parts, I tweet them out one by one.

Quite a cumbersome work, where ThreadStart can do the heavy lifting for me instead:

Creating a Twitter thread without guidance is hard. ThreadStart makes it easy for people to create great content on Twitter. We’ll help you craft a killer set of tweets!

Let’s test it out soon!

ThreadStart →

Too Many SVGs Clogging Up Your Markup? Try use

Good reminder by Georgi Nikoloff to have one (visually hidden) SVG that contains several layers, which you can then include further down in your code.

SVG has a <defs> tag that lets us declare something like our graph footer just once and then simply reference it — using <use> — from anywhere on the page to render it as many times as we want.

That way you end up with less DOM nodes, less bytes transferred, and less memory consumed.

Too Many SVGs Clogging Up Your Markup? Try use

Manage your DNS from GitHub with DNSControl

In this post Sven Luijten uses DNSControl from within GitHub Actions to manage DNS:

What if you could edit a JavaScript file with your desired DNS configuration, submit a pull request on GitHub, see a preview of the changes you are about to make, and when you merge it, have those changes be applied completely automatically?

The main action behind it is koenrh/dnscontrol-action:

# .github/workflows/push.yml
name: Push DNS changes

      - main

    runs-on: ubuntu-latest
      - uses: actions/checkout@v2

      - name: Push DNS changes
        uses: koenrh/dnscontrol-action@v3
          args: push

The action also supports more arguments — as covered in Sven’s post — for checking and previewing.

Manage your DNS from GitHub with DNSControl →

Debugging Layout Shifts

Over at, Katie Hempenius learns us how to identify and fix layout shifts using the Layout Instability API and the DevTools.

What I take away from this is that you can easily spot them using DevTools: In the Rendering Panel you can enable an option to highlight areas of Layout Shift:

To enable Layout Shift Regions in DevTools, go to Settings → More Tools → Rendering → Layout Shift Regions then refresh the page that you wish to debug. Areas of layout shift will be briefly highlighted in purple.

Debugging layout shifts →

Just-In-Time: The Next Generation of Tailwind CSS

Adam Wathan from Tailwind:

One of the hardest constraints we’ve had to deal with as we’ve improved Tailwind CSS over the years is the generated file size in development. With enough customizations to your config file, the generated CSS can reach 10mb or more, and there’s only so much CSS that build tools and even the browser itself will comfortably tolerate.

Today I’m super excited to share a new project we’ve been working on that makes this constraint a thing of the past: a just-in-time compiler for Tailwind CSS.

If you’re using Tailwind, then the JIT Compiler — Sherlocked from Windi CSS (ref) — will be a very welcome gift. Using it you can basically drop all variants from your config, improve build-times and filesize.

Today our intern Elian integrated @tailwindcss/jit into a Tailwind-based project we’re working on. Compile times dropped by 60% (from ±25s to ±10s) and filesize dropped by 90% (from 918kB to 84kB), as detailed on his blog.

😱 In case you think I’ve switched over to Tailwind: No, I’m still no fan of Tailwind and — unless you use it with @apply — would not recommend using it. On the other hand I do see that it allows one to iterate quickly while prototyping and that is a very welcome gift for developers who are somewhat familiar with CSS. If that floats your boat, then that’s fine. If it doesn’t, then that’s fine too 🙂

However, if one were to ask me to choose between the two, I will always recommend one to learn CSS. That knowledge is relevant today, tomorrow, and still will be 10 years from now, when Tailwind is long gone.

CSS background-clip Demo: Text with Animated Emoji

Fun demo by Elad Shechter:

See the Pen Background-clip: text with Animated Emoji by Elad Shechter (@elad2412) on CodePen.

Behind it are two techniques:

  1. The 👻 emoji is set as the background-image of the text. To do so one must wrap it inside an SVG, and successively inject the SVG using a Data URL.

  2. To clip the emoji to the foreground text, he uses background-clip: text.

I like the fact that these techniques by themselves are not new, yet their combination is (at least to me this was the case):

  1. The “Emoji in SVG”-technique allows you to have an Emoji Mouse Cursor or use an emoji as favicon.
  2. Using background-clip: text works on any background image. As gradients can also be set as background images, you can create gradient links.

SWR — React Hooks library for data fetching

From the Next.js folks comes SWR, a React Hooks library for data fetching built on Suspense:

The name “SWR” is derived from stale-while-revalidate, a cache invalidation strategy popularized by HTTP RFC 5861. SWR first returns the data from cache (stale), then sends the fetch request (revalidate), and finally comes with the up-to-date data again.

It can avoid scenarios where you need to pass down a fetched user into several child components

import useSWR from 'swr'

const fetcher = (...args) => fetch(...args).then(res => res.json())

function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
// page component
function Page () {
  return <div>
    <Navbar />
    <Content />

// child components
function Navbar () {
  return <div>
    <Avatar />

function Content () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <h1>Welcome back, {}</h1>

function Avatar () {
  const { user, isLoading } = useUser()
  if (isLoading) return <Spinner />
  return <img src={user.avatar} alt={} />

With SWR being used, this will result in only 1 request to /api/user/${id}.

The most beautiful thing is that there will be only 1 request sent to the API, because they use the same SWR key and the request is deduped, cached and shared automatically.

SWR — React Hooks library for data fetching →