Custom Property Fallbacks

Look at this CSS and take a guess what color our text will be. No tricks, this is the only relevant code:

:root {
  --color: green;
  --color: notacolor;
  color: red;
  color: var(--color, blue);
}Code language: CSS (css)

Perhaps surprisingly, the answer is the default text color, usually black. Let’s figure out why that’s the case and how to write a fallback that works.

var() Fallbacks

The value blue seems a likely candidate to be our color, doesn’t it? After all, we often call the 2nd parameter of var() its “fallback” value. However, this type of fallback is only used in the following circumstances:

  • The custom property isn’t defined.
  • The custom property’s value is the initial keyword.
  • The custom property is animation-tainted and is used in an animation property.

and

  • The custom property isn’t registered with @property.

Since a registered custom property must be given an initial value, it’ll always have a valid value and will never use its fallback.

Our custom property is defined, not animated tainted, and its value isn’t a keyword, so we can throw away blue and keep looking.

Fallback Declarations

Usually in CSS, we can rely on the cascade to fallback to a previous valid value, leading to a common pattern where we declare the property twice:

overflow: hidden;
overflow: clip;Code language: CSS (css)

Browsers that don’t support the clip keyword will discard the entire declaration and use hidden instead.

Unfortunately, custom properties don’t work like this.

When parsing a custom property or its matching var() function, the browser doesn’t know if it’s valid until it comes time to compute its value. So instead they are treated as always valid, and any previous declarations get discarded.

If you want to prevent a custom property from being overwritten, you’ll have to mark it as important.

That means --color: green; gets discarded immediately upon discovering --color: notacolor;, and color: red; is discarded when we get to color: var(--notacolor, blue);.

In the end, our CSS computes to:

color: notacolor;Code language: CSS (css)

Unsurprisingly, this isn’t valid, and that’s why we get black as our color.

What We Can Do Instead

That all sounds bad but we actually have several options for writing fallbacks that work.

@property

As mentioned before, if a registered custom property is invalid, it’ll always use its initial-value, which means we can use that as our fallback:

@property --color {
  syntax: '<color>';
  inherits: true;
  initial-value: purple;
}

:root {
  --color: notacolor;
  color: var(--color); 
}Code language: CSS (css)

Exactly what we want, though:

  • We can only define a single fallback for the entire document (which can be good enough depending on how your custom properties are organised).
  • The intent isn’t very obvious, registering a property is a roundabout way of setting a fallback.

If that’s not a problem for you and/or you’re registering your properties anyway, this is a great option.

@supports

Using @supports lets us check our value is valid before declaring it and that gives us even more flexibility in how we define our fallbacks. Let’s look at two ways to use it:

Set a safe value first, and then inside an @supports block we can redeclare the property:

:root {
  --color: red;
}

@supports (color: notacolor) {
  :root {
    --color: notacolor;
  }
}Code language: CSS (css)

Then we can just use it anywhere we like, without having to think about the fallback:

p {
  color: var(--color);
}Code language: CSS (css)

We can set it and forget it, confident that when our value isn’t supported we’ll still have our previous declaration to fall back on. 9 out of 10 times this is what I reach for.

Alternatively, let’s skip setting a safe value, and only define the property inside @supports:

@supports (color: notacolor) {
  :root {
    --color: notacolor;
  }
}Code language: CSS (css)

Where’s our fallback? Well, since our property only gets declared when it’s supported, we can use the 2nd parameter of var() to write our fallback inline:

p {
  color: var(--color, red);
}Code language: CSS (css)

If you find yourself wanting to use different fallbacks for the same custom property, this could be a better option.

The Future

Eventually, we’ll have even more options for dealing with invalid values.

New CSS goodies like like the keyword revert-rule and the first-valid() function will let us do away with @supports and write our fallbacks wherever we want:

:root {
  /* Multiple inline fallbacks when declaring the property */
  --color: first-valid(notacolor, maybeacolor, red);
}

p {
  /* Fallback to a different rule when using the variable */
  color: first-valid(var(--color), revert-rule);
}Code language: CSS (css)

You can follow their progress on GitHub:

  • Discussion resulting in the revert-rule keyword resolution.
  • Discussion resulting in first-valid() resolution.

Further Learning

Wanna learn CSS from a course?

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $363,806 contributed to date.