CSS variables (or custom properties, whichever is more convenient) were originally conceived to store repeating properties, such as color palettes or fonts, in one place. Working with variables in preprocessors is much more flexible, but the magic of SASS/SCSS cannot always be applied. In the real world, we often do without them, which may lead to code bloat, as well as unnecessary files and excess formatting. In this article, we will consider several interesting hacks that allow you to use custom properties for what would seem impossible without preprocessors or JS.
Defining themes in pure CSS is not the most pleasant experience: switching to a dark palette usually requires changing colors for many elements, i.e. backgrounds, texts, links, buttons, etc. The user’s initial preferences are obtained using the prefers-color-scheme media query, inside which new colors need to be placed for all selectors, which leads to bloating:
:root {
--background: #fff;
--text-color: #000;
--link-color: #0089c7;
--primary-color: #165fb9;
/* ... */
}
@media (prefers-color-scheme: dark) {
:root {
--background: #1b1b1b);
--text-color: #eaeaea;
--link-color: #b76c10;
--primary-color: #8916b9;
/* the same thing for dozens of lines in different scopes*/
}
}
There is no other mechanism in CSS to change variables, but repetition can still be avoided with additional values:
--background: var(--light, #fff) var(--dark, #1b1b1b);
If the
--light
value is set to initial and the --dark
value is valid but inapplicable, --background
will get the color #fff. CSS has the value that would be great for this situation, and that’s... the whitespace. Thus, for the light theme, the line will be parsed like this:--background: #fff;
And for the dark theme, it will look like this:
--background: #1b1b1b;
Note the spaces – they do not break the syntax (which would reset the entire line definition). Now all that remains is to move the state switch into separate variables:
:root {
/* --ON and --OFF replace the binary variable */
--ON: initial;
--OFF: ;
}
/* select the light theme by default */
.theme-default,
.theme-light {
--light: var(--ON);
--dark: var(--OFF);
}
.theme-dark {
--light: var(--OFF);
--dark: var(--ON);
}
/* the media query is now only needed for switching */
@media (prefers-color-scheme: dark) {
.theme-default {
--light: var(--OFF);
--dark: var(--ON);
}
}
Now color schemes can be defined in one place, it will look like this:
:root {
--background: var(--light, #fff) var(--dark, #1b1b1b);
--text-color: var(--light, #000) var(--dark, #eaeaea);
--link-color: var(--light, #0089c7) var(--dark, #b76c10);
--primary-color: var(--light, #165fb9) var(--dark, #8916b9);
/* ... */
}
This code is less descriptive than the classic definition, but it is easy to get used to. It not only saves a lot of space but also reduces the chance of mistakes when changing.
As we recall, there are no explicit conditionals in CSS, except for media queries, to manipulate the state. But the structure of this language sometimes gives opportunities when no one assumes that. Meet switch-case for animation!
Any number of keyframes (
@keyframes
) can be created for the animation
property. They can be used as a persistent store of state if you keep the animation paused. You need to know the exact delay for each frame so that the paused animation shows the required moment instead of being fixed on the first frame. Here’s a good example: https://jsfiddle.net/keb1f5g7/1/Let’s analyze the principle of work:
animation-play-state: paused
.animation-delay
causes the animation to stop at a specific frame (or between two specific frames, as the gradient of the first slider works). The slider values range from -100s to 0s.animation-duration
, you can specify any convenient number, but you need to remember that when the last frame is being played, the animation turns off. So, the maximum duration should not coincide in time with the last defined frame (case). Therefore, in the example above, the extent of the slider is 100 seconds with a total duration of 100.001s.In the first trick, we have already used the
--ON
--OFF
variables instead of a binary variable. You can use custom properties to store numeric values and get 0 or 1 in a variety of scenarios by using calc() and clamp() to calculate various parameters. The explicit assignment is quite inconvenient for even inverting values, as in the example above, and trying to find some kind of logic here is a total nightmare. It’s good that basic Boolean operations can be performed directly in variable declarations!not
That’s simple, 1 - 0 = 1, 1 - 1 = 0
--not: calc(1 - var(--j))
and
Simple multiplication:
0 * 0 = 0
1 * 0 = 0
0 * 1 = 0
1 * 1 = 0
--and: calc(var(--k)*var(--i))
nand
1 – and = inverted and
--nand: calc(1 - var(--k)*var(--i))
or
If at least one of the operands evaluates to 1, or returns 1:
k or i = (not k) nand (not i)
--or: calc(1 - (1 - var(--k))*(1 - var(--i)))
nor
Similar to nand, nor = 1 – or:
--nor: calc((1 - var(--k))*(1 - var(--i)))
xor
Returns 1 if exactly one of the operands evaluates to 1:
--xor: calc((var(--k) - var(--i))*(var(--k) - var(--i)))
With binary logic and conditional operators in hand, you can use CSS to implement lots of things that previously seemed possible only by involving JS. But there is one aspect – the deeper you go, the less readable the code becomes and the more difficult it is to write and maintain. So, the list of such tricks can be continued even further, but most of them will almost certainly not be useful in the real world. However, the basic concepts such as switch and or will help you get by with a nice CSS-only solution even if it seemed impossible before.