How do I apply opacity to a CSS color variable?

JoshyRobot picture JoshyRobot · Oct 13, 2016 · Viewed 78.2k times · Source

I am designing an app in electron, so I have access to CSS variables. I have defined a color variable in vars.css:

:root {
  --color: #f0f0f0;
}

I want to use this color in main.css, but with some opacity applied:

#element {
  background: (somehow use var(--color) at some opacity);
}

How would I go about doing this? I am not using any preprocessor, only CSS. I would prefer an all-CSS answer, but I will accept JavaScript/jQuery.

I cannot use opacity because I am using a background image that should not be transparent.

Answer

BoltClock picture BoltClock · Dec 21, 2016

You can't take an existing color value and apply an alpha channel to it. Namely, you can't take an existing hex value such as #f0f0f0, give it an alpha component and use the resulting value with another property.

However, custom properties allow you to convert your hex value into an RGB triplet for use with rgba(), store that value in the custom property (including the commas!), substitute that value using var() into an rgba() function with your desired alpha value, and it'll just work:

:root {
  /* #f0f0f0 in decimal RGB */
  --color: 240, 240, 240;
}

body {
  color: #000;
  background-color: #000;
}

#element {
  background-color: rgba(var(--color), 0.8);
}
<p id="element">If you can see this, your browser supports custom properties.</p>

This seems almost too good to be true.1 How does it work?

The magic lies in the fact that the values of custom properties are substituted as is when replacing var() references in a property value, before that property's value is computed. This means that as far as custom properties are concerned, the value of --color in your example isn't a color value at all until a var(--color) expression appears somewhere that expects a color value (and only in that context). From section 2.1 of the css-variables spec:

The allowed syntax for custom properties is extremely permissive. The <declaration-value> production matches any sequence of one or more tokens, so long as the sequence does not contain <bad-string-token>, <bad-url-token>, unmatched <)-token>, <]-token>, or <}-token>, or top-level <semicolon-token> tokens or <delim-token> tokens with a value of "!".

For example, the following is a valid custom property:

--foo: if(x > 5) this.width = 10;

While this value is obviously useless as a variable, as it would be invalid in any normal property, it might be read and acted on by JavaScript.

And section 3:

If a property contains one or more var() functions, and those functions are syntactically valid, the entire property’s grammar must be assumed to be valid at parse time. It is only syntax-checked at computed-value time, after var() functions have been substituted.

This means that the 240, 240, 240 value you see above gets substituted directly into the rgba() function before the declaration is computed. So this:

#element {
  background-color: rgba(var(--color), 0.8);
}

which doesn't appear to be valid CSS at first because rgba() expects no less than four comma-separated numeric values, becomes this:

#element {
  background-color: rgba(240, 240, 240, 0.8);
}

which, of course, is perfectly valid CSS.

Taking it one step further, you can store the alpha component in its own custom property:

:root {
  --color: 240, 240, 240;
  --alpha: 0.8;
}

and substitute it, with the same result:

#element {
  background-color: rgba(var(--color), var(--alpha));
}

This allows you to have different alpha values that you can swap around on-the-fly.


1 Well, it is, if you're running the code snippet in a browser that doesn't support custom properties.