CSS: local VS global variables
Using CSS custom properties (aka variables) is a great way to create themes, but it also enables what’s IMHO quite possibly one of the only valid use cases for inline style
attributes, as it brings the following advantages:
- it’s SSR compatible out of the box
- it doesn’t affect the native look and feel until the theme is loaded
- it doesn’t affect the layout if, for whatever reason, the external CSS resource failed due bad network conditions, or even CDN issues
What’s being covered in here, is a technique to localize custom properties that plays well with inheritance, as properties, like anything else, can affect inner nodes, but it doesn’t have to be like that.
CSS properties in action
Let’s take the following case as example:
<div>
Outer
<div style="
--background-color: rgba(150, 150, 255, .5);
">
Inner
<div>
Nested
</div>
</div>
</div>
If we use the background-color: var(--background-color, initial);
approach, which “suffer” inheritance, we’ll see two different stripes.
However, if we use the locally scoped property/variable approach, the nested <div>
would not be affected by its outer container.
How does it work?
It’s pretty simple: every target node will have its own property, which will have higher priority over the one defined in any outer component, granting a possible default behavior, hence portable for literally every property we need, including padding
, margin
, border
, and so on and so forth.
local VS global
The “local” version of this technique is !important
proof: no matter if the outer container used !important
, the inner <div>
won’t be affected.
This makes such technique an ideal companion for Custom Elements, without needing to use any Shadow DOM to ensure styles are applied as desired.
On the other hand, the “global” approach can be overruled via !important
, either via the external file or through the inline style
declaration.
The “global” approach though, is more suitable for generic rules, as example having a consistent border-radius: var(--border-radius, initial)
that would rarely need to change, and the same goes for color
or font-family
, still providing the ability to define, within a specific element, a different kind of property.
div {
color: var(--color, silver);
}.darker {
--color: black;
}
With this technique, all it takes to change color to every <div class="darker">
and every possible nested div, is to change the --color
property of the darker, so that all others will inherit such property without falling back to silver
.
Composable
These techniques can be combined with classes too, and the only change to our daily CSS code is that we need to think in variables.
Only those very specific cases where we want different defaults, or animate some property via JS, could use node.style.cssText = '--height: 200px';
, as example, still without affecting elements, if the “local” approach is used.
In short, it’s a new approach to CSS variables that shouldn’t be abused, but can deliver great achievements, and frankly simplify most custom/ad-hoc changes for this or that element, without forcing us to create thousand classes with names rarely as semantic as an explicit --background-color
is.
What about IE?
The day we’ll stop asking this question will be a great win for the Web. However, there are at least 4ways to solve the IE issue:
- do nothing, as the technique is graceful enhancement out of the box, so that old IE will simply look natively OKish
- transform the modern CSS into IE compatible CSS, loosing the ability to change, or define variables at runtime, but IE is not good with animations anyway (preferred method)
- use this polyfill for IE11 and call it a day (also suggested)
- define properties before specifying variables, so that modern browsers will be using variables, while IE will ignore the rest
div {
color: silver;
color: var(--color, silver) !important;
}.darker {
color: black;
--color: black;
}
Once again, these examples are not suggesting what you should really do in production, are simply demoing the approach with both modern and older browsers.