{"id":274807,"date":"2018-12-05T07:38:34","date_gmt":"2018-12-05T14:38:34","guid":{"rendered":"http:\/\/css-tricks.com\/?p=274807"},"modified":"2018-12-06T08:26:54","modified_gmt":"2018-12-06T15:26:54","slug":"dry-switching-with-css-variables-the-difference-of-one-declaration","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/dry-switching-with-css-variables-the-difference-of-one-declaration\/","title":{"rendered":"DRY Switching with CSS Variables: The Difference of One Declaration"},"content":{"rendered":"
This is the first post of a two-part series that looks into the way CSS variables can be used to make the code for complex layouts and interactions less difficult to write and a lot easier to maintain. This first installment walks through various use cases where this technique applies. The second post<\/a> covers the use of fallbacks and invalid values to extend the technique to non-numeric values.<\/p>\n What if I told you a single CSS declaration makes the difference in the following image between the wide screen case (left) and the second one (right)? And what if I told you a single CSS declaration makes the difference between the odd and even items in the wide screen case?<\/p>\n <\/p>\n Or that a single CSS declaration makes the difference between the collapsed and expanded cases below?<\/p>\n How is that even possible?<\/p>\n Well, as you may have guessed from the title, it’s all in the power of CSS variables.<\/p>\n There are already plenty of articles out there on what CSS variables are and how to get started with them, so we won’t be getting into that here.<\/p>\n Instead, we’ll dive straight into why CSS variables are useful for achieving these cases and others, then we’ll move on to a detailed explanation of the how<\/em> for various cases. We’ll code an actual example from scratch, step by step, and, finally, you’ll be getting some eye candy in the form of a few more demos that use the same technique.<\/p>\n So let’s get started!<\/p>\n For me, the best thing about CSS variables is that they’ve opened the door for styling things in a logical, mathematical and effortless way.<\/p>\n One example of this is the CSS variable version of the yin and yang loader<\/a> I coded last year. For this version, we create the two halves with the two pseudo-elements of the loader element.<\/p>\n We use the same Without CSS variables, we’d have to set all these properties ( In the particular case of the yin and yang loader, all the properties we change between the two halves (pseudo-elements) go from a zero value for one state of the switch and a non-zero value for the other state.<\/p>\n If we want our value to be zero when the switch is off ( However, if we want our value to be non-zero when the switch is off ( You can see this concept illustrated below:<\/p>\n For the particular case of the loader, we use HSL values for The hues go around the bicone, The saturation goes from The “same horizontal plane” means having the same lightness, which increases along the vertical bicone axis, going from Since we only need Note that this approach<\/a> doesn’t work in Edge due to the fact that Edge doesn’t support But what if we want to have a non-zero value when the switch is off ( Let’s say we want an element to have a grey The first thing we do is switch from hex to a more manageable format such as We could do this manually either by using a tool such as Lea Verou’s CSS Colors<\/a> or via DevTools. If we have a Even better, if we’re using Sass, we can extract the components with While So we extract the three components of the Note that we’ve rounded the results of the Now we have that:<\/p>\n We can write our two backgrounds as:<\/p>\n Using the switch variable Here, we’ve denoted by The formula above works for switching in between any two HSL values. However, in this particular case, we can simplify it because we have a pure grey when the switch is off ( Purely grey values have equal red, green and blue values when taking into account the RGB model.<\/p>\n When taking into account the HSL model, the hue is irrelevant (our grey looks the same for all hues), the saturation is always In this situation, we can always keep the hue of the non-grey value (the one we have for the “on” case, Since the saturation of any grey value (the one we have for the “off” case, This leaves the lightness as the only component where we still need to apply the full formula.<\/p>\n The above can be tested in this demo<\/a>.<\/p>\n Similarly, let’s say we want the Alright, let’s now move on to clearing another aspect of this: what is it exactly that causes the switch to flip from on to off or the other way around?<\/p>\n We have a few options here.<\/p>\n This means the switch is off for certain elements and on for other elements. For example, this can be determined by parity. Let’s say we want all the even elements to be rotated and have an orange In the parity case, we flip the switch on for every second item ( This means the switch is off when the element itself (or a parent or one of its previous siblings) is in one state and off when it’s another state. In the interactive examples from the previous section, the switch was flipped when a checkbox before our element got checked or unchecked.<\/p>\n We can also have something like a white link that scales up and turns orange when focused or hovered:<\/p>\n Since Another possibility is that switching is triggered by a media query, for example, when the orientation changes or when going from one viewport range to another.<\/p>\n Let’s say we have a Why CSS variables are useful<\/h3>\n
background<\/code>,
border-color<\/code>,
transform-origin<\/code> and
animation-delay<\/code> values for the two halves. These values all depend on a switch variable
--i<\/code> that’s initially set to
0<\/code> on both halves (the pseudo-elements), but then we change it to
1<\/code> for the second half (the
:after<\/code> pseudo-element), thus dynamically modifying the computed values of all these properties.<\/p>\n
border-color<\/code>,
transform-origin<\/code>,
background<\/code>,
animation-delay<\/code>) again on the
:after<\/code> pseudo-element and risk making some typo or even forgetting to set some of them.<\/p>\n
How switching works in the general case<\/h3>\n
Switching between a zero and a non-zero value<\/h4>\n
--i: 0<\/code>) and non-zero when the switch is on (
--i: 1<\/code>), then we multiply it with the switch value (
var(--i)<\/code>). This way, if our non-zero value should be, let’s say an angular value of
30deg<\/code>, we have:<\/p>\n
\n
--i: 0<\/code>),
calc(var(--i)*30deg)<\/code> computes to
0*30deg = 0deg<\/code><\/li>\n
--i: 1<\/code>),
calc(var(--i)*30deg)<\/code> computes to
1*30deg = 30deg<\/code><\/li>\n<\/ul>\n
--i: 0<\/code>) and zero when the switch is on (
--i: 1<\/code>), then we multiply it with the complementary of the switch value (
1 - var(--i)<\/code>). This way, for the same non-zero angular value of
30deg<\/code>, we have:<\/p>\n
\n
--i: 0<\/code>),
calc((1 - var(--i))*30deg)<\/code> computes to
(1 - 0)*30deg = 1*30deg = 30deg<\/code><\/li>\n
--i: 1<\/code>),
calc((1 - var(--i))*30deg)<\/code> computes to
(1 - 1)*30deg = 0*30deg = 0deg<\/code><\/li>\n<\/ul>\n
calc()<\/code> not working for angle values)<\/figcaption><\/figure>\n
border-color<\/code> and
background-color<\/code>. HSL stands for hue, saturation, lightness and can be best represented visually with the help of a bicone (which is made up of two cones with the bases glued together).<\/p>\n
0\u00b0<\/code> being equivalent to
360\u00b0<\/code> to give us a red in both cases.<\/p>\n
0%<\/code> on the vertical axis of the bicone to
100%<\/code> on the bicone surface. When the saturation is
0%<\/code> (on the vertical axis of the bicone), the hue doesn’t matter anymore; we get the exact same grey for all hues in the same horizontal plane.<\/p>\n
0%<\/code> at the
black<\/code> bicone vertex to
100%<\/code> at the
white<\/code> bicone vertex. When the lightness is either
0%<\/code> or
100%<\/code>, neither the hue nor the saturation matter anymore – we always get
black<\/code> for a lightness value of
0%<\/code> and
white<\/code> for a lightness value of
100%<\/code>.<\/p>\n
black<\/code> and
white<\/code> for our ☯ symbol, the hue and saturation are irrelevant, so we zero them and then switch between
black<\/code> and
white<\/code> by switching the lightness between
0%<\/code> and
100%<\/code>.<\/p>\n
.yin-yang {\r\n \/* other styles that are irrelevant here *\/\r\n \r\n &:before, &:after {\r\n \/* other styles that are irrelevant here *\/\r\n --i: 0;\r\n\r\n \/* lightness of border-color when \r\n * --i: 0 is (1 - 0)*100% = 1*100% = 100% (white)\r\n * --i: 1 is (1 - 1)*100% = 0*100% = 0% (black) *\/\r\n border: solid $d\/6 hsl(0, 0%, calc((1 - var(--i))*100%));\r\n\r\n \/* x coordinate of transform-origin when \r\n * --i: 0 is 0*100% = 0% (left) \r\n * --i: 1 is 1*100% = 100% (right) *\/\r\n transform-origin: calc(var(--i)*100%) 50%;\r\n\r\n \/* lightness of background-color when \r\n * --i: 0 is 0*100% = 0% (black) \r\n * --i: 1 is 1*100% = 100% (white) *\/\r\n background: hsl(0, 0%, calc(var(--i)*100%));\r\n\r\n \/* animation-delay when\r\n * --i: 0 is 0*-$t = 0s \r\n * --i: 1 is 1*-$t = -$t *\/\r\n animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;\r\n }\r\n\t\r\n &:after { --i: 1 }\r\n}<\/code><\/pre>\n
calc()<\/code> values for
animation-delay<\/code>.<\/p>\n
--i: 0<\/code>) and another different<\/em> non-zero value when the switch is on (
--i: 1<\/code>)?<\/p>\n
Switching between two non-zero values<\/h4>\n
background<\/code> (
#ccc<\/code>) when the switch is off (
--i: 0<\/code>) and an orange
background<\/code> (
#f90<\/code>) when the switch is on (
--i: 1<\/code>).<\/p>\n
rgb()<\/code> or
hsl()<\/code>.<\/p>\n
background<\/code> set on an element we can cycle through formats by keeping the
Shift<\/code> key pressed while clicking on the square (or circle) in front of the value in DevTools. This works in both Chrome and Firefox, though it doesn’t appear to work in Edge.<\/p>\n
value.”\/>
red()<\/code><\/a>\/
green()<\/code><\/a>\/
blue()<\/code><\/a> or
hue()<\/code><\/a>\/
saturation()<\/code><\/a>\/
lightness()<\/code><\/a> functions.<\/p>\n
rgb()<\/code> may be the better known format, I tend to prefer
hsl()<\/code> because I find it more intuitive and it’s easier for me to get an idea about what to expect visually just by looking at the code.<\/p>\n
hsl()<\/code> equivalents of our two values (
$c0: #ccc<\/code> when the switch is off and
$c1: #f90<\/code> when the switch is on) using these functions:<\/p>\n
$c0: #ccc;\r\n$c1: #f90;\r\n\r\n$h0: round(hue($c0)\/1deg);\r\n$s0: round(saturation($c0));\r\n$l0: round(lightness($c0));\r\n\r\n$h1: round(hue($c1)\/1deg);\r\n$s1: round(saturation($c1));\r\n$l1: round(lightness($c1))<\/code><\/pre>\n
hue()<\/code>,
saturation()<\/code> and
lightness()<\/code> functions as they may return a lot of decimals and we want to keep our generated code clean. We’ve also divided the result of the
hue()<\/code> function by
1deg<\/code>, as the returned value is a degree value in this case and Edge only supports unit-less values inside the CSS<\/em><\/strong>
hsl()<\/code> function. Normally, when using Sass, we can have degree values, not just unit-less ones for the hue inside the
hsl()<\/code> function because Sass treats it as the Sass
hsl()<\/code> function, which gets compiled into a CSS
hsl()<\/code> function with a unit-less hue. But here, we have a dynamic CSS variable inside, so Sass treats this function as the CSS
hsl()<\/code> function that doesn’t get compiled into anything else, so, if the hue has a unit, this doesn’t get removed from the generated CSS.<\/p>\n
\n
--i: 0<\/code>), our
background<\/code> is
hsl($h0, $s0, $l0)<\/code><\/li>\n
--i: 1<\/code>), our
background<\/code> is
hsl($h1, $s1, $l1)<\/code><\/li>\n<\/ul>\n
\n
--i: 0<\/code>),
hsl(1*$h0 + 0*$h1, 1*$s0 + 0*$s1, 1*$l0 + 1*$l1)<\/code><\/li>\n
--i: 1<\/code>),
hsl(0*$h0 + 1*$h1, 0*$s0 + 1*$s1, 0*$l0 + 1*$l1)<\/code><\/li>\n<\/ul>\n
--i<\/code>, we can unify the two cases:<\/p>\n
--j: calc(1 - var(--i));\r\nbackground: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), \r\n calc(var(--j)*#{$s0} + var(--i)*#{$s1}), \r\n calc(var(--j)*#{$l0} + var(--i)*#{$l1}))<\/code><\/pre>\n
--j<\/code> the complementary value of
--i<\/code> (when
--i<\/code> is
0<\/code>,
--j<\/code> is
1<\/code> and when
--i<\/code> is
1<\/code>,
--j<\/code> is
0<\/code>).<\/p>\n
--i: 0<\/code>).<\/p>\n
0%<\/code> and only the lightness matters, determining how light or dark our grey is.<\/p>\n
$h1<\/code>). <\/p>\n
$s0<\/code>) is always
0%<\/code>, multiplying it with either
0<\/code> or
1<\/code> always gives us
0%<\/code>. So, given the
var(--j)*#{$s0}<\/code> term in our formula is always
0%<\/code>, we can just ditch it and our saturation formula reduces to the product between the saturation of the “on” case
$s1<\/code> and the switch variable
--i<\/code>.<\/p>\n
--j: calc(1 - var(--i));\r\nbackground: hsl($h1, \r\n calc(var(--i)*#{$s1}), \r\n calc(var(--j)*#{$l0} + var(--i)*#{d1l}))<\/code><\/pre>\n
font-size<\/code> of some text to be
2rem<\/code> when our switch is off (
--i: 0<\/code>) and
10vw<\/code> when the switch is on (
--i: 1<\/code>). Applying the same method, we have:<\/p>\n
font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)<\/code><\/pre>\n
What triggers switching<\/h3>\n
Element-based switching<\/h4>\n
background<\/code> instead of the initial grey one.<\/p>\n
.box {\r\n --i: 0;\r\n --j: calc(1 - var(--i));\r\n transform: rotate(calc(var(--i)*30deg));\r\n background: hsl($h1, \r\n calc(var(--i)*#{$s1}), \r\n calc(var(--j)*#{$l0} + var(--i)*#{$l1}));\r\n \r\n &:nth-child(2n) { --i: 1 }\r\n}<\/code><\/pre>\n
calc()<\/code> not working for angle values)<\/figcaption><\/figure>\n
:nth-child(2n)<\/code>), but we can also flip it on for every seventh item (
:nth-child(7n)<\/code>), for the first two items<\/a> (
:nth-child(-n + 2)<\/code>), for all items except the first and last two<\/a> (
:nth-child(n + 3):nth-last-child(n + 3)<\/code>). We can also flip it on just for headings or just for elements that have a certain attribute.<\/p>\n
State-based switching<\/h4>\n
$c: #f90;\r\n\r\n$h: round(hue($c)\/1deg);\r\n$s: round(saturation($c));\r\n$l: round(lightness($c));\r\n\r\na {\r\n --i: 0;\r\n transform: scale(calc(1 + var(--i)*.25));\r\n color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));\r\n \r\n &:focus, &:hover { --i: 1 }\r\n}<\/code><\/pre>\n
white<\/code> is any
hsl()<\/code> value with a lightness of
100%<\/code> (the hue and saturation are irrelevant), we can simplify things by always keeping the hue and saturation of the
:focus<\/code>\/
:hover<\/code> state and only changing the lightness.<\/p>\n
calc()<\/code> values not being supported inside
scale()<\/code> functions)<\/figcaption><\/figure>\n
Media query-based switching<\/h4>\n
white<\/code> heading with a
font-size<\/code> of