When it comes to theming a web application, CSS variables turn out to be the simplest and most convenient way. And this is quite logical - you can set the value of a variable once and use it everywhere. But it seems to me that the potential of CSS variables is much broader.
Local variables
The @property CSS at-rule allows to create variables which are not inherits. Thus, these variables are essentially local. This allows you to customize the styles of specific HTML elements without worrying about a cascade of CSS rules. For example, I want to describe a box style where I can configure border radius and background color:
@property --box-radius {
syntax: "*";
inherits: false;
initial-value: 16px;
}
@property --box-background {
syntax: "*";
inherits: false;
initial-value: #a2a3a3;
}
.box {
background: var(--box-background);
border-radius: var(--box-radius);
}
.box-dark {
--box-background: #626262;
}
.box-light {
--box-background: #d4d6d7;
}
.box-rad-s {
--box-background: 8px;
}
.box-rad-l {
--box-background: 32px;
}
Now, if you put one box inside another, the parent element will not override the appearance of the child. In other words, they will have an independent states:
<div class="box box-light box-rad-s">
Parent box with customized property values
<div class="box">
Child box with initial property values
</div>
</div>
I think you've noticed that the color and size values in the code example are fixed. The best way would be to use global variables as values.
Global variables
The idea is very simple - local variables can take values defined in global variables:
:root {
--global-background: light-dark(#dedede, #676767);
--global-radius: 16px;
}
@property --box-radius {
syntax: "*";
inherits: false;
}
@property --box-background {
syntax: "*";
inherits: false;
}
.box {
background: var(--box-background, var(--global-background));
border-radius: var(--box-radius, var(--global-radius));
}
.box-dark {
--box-background: oklch(from var(--global-background) calc(l - 0.2) c h / alpha);
}
.box-light {
--box-background: oklch(from var(--global-background) calc(l + 0.2) c h / alpha);
}
.box-rad-s {
--box-radius: calc(var(--global-radius) / 2);
}
.box-rad-l {
--box-radius: calc(var(--global-radius) * 2);
}
Now we can have any number of local variables - the entire theme will be determined by only a few global values. But we're losing control of the local variables. If each of them uses its own set of calculations, then this will break the very idea of themization - the styles will not be uniform. So, how to limit them?
Computed variables
Computed variables link global and local variables. They act as a compromise between a large number of theme settings and arbitrary local transformations:
:root {
/* global vars */
--global-background: light-dark(#dedede, #676767);
--global-radius: 16px;
/* computed vars */
--radius-s: calc(0.5 * var(--global-radius));
--radius-m: var(--global-radius);
--radius-l: calc(2 * var(--global-radius));
--background-normal: var(--global-background);
--background-dark: oklch(from var(--global-background) calc(l - 0.2) c h / alpha);
--background-light: oklch(from var(--global-background) calc(l + 0.2) c h / alpha);
}
/* local vars */
@property --box-radius {
syntax: "*";
inherits: false;
}
@property --box-background {
syntax: "*";
inherits: false;
}
.box {
--box-background: var(--background-normal);
--box-radius: var(--radius-m);
background: var(--box-background);
border-radius: var(--box-radius);
}
.box-dark {
--box-background: var(--background-dark);
}
.box-light {
--box-background: var(--background-light);
}
.box-rad-s {
--box-radius: var(--radius-s);
}
.box-rad-l {
--box-radius: var(--radius-l);
}
Thus, our theme has only six variants for internal styles. Please note that now global and local variables do not interact directly - this is how we separate the external API of the theme and its internal implementation. We impose restrictions to make the styles predictable and uniform.
Final thoughts
I think using three layers of CSS variables makes it easier to customize theme and extend it. It looks like a kind of the Open-Closed Principle -
you can extend theme with new CSS rules and local variables but your global variables remain constant.
In the future the layer of computed variables can be implemented via @function at-rule. This will really protect them from being changed.
Of course, the possibilities of theming are much wider when using JavaScript, with the help of TypeScript, styling becomes more obvious., but that's a completely different story.
Enjoy your frontend development!

Top comments (0)