DEV Community

Cover image for The Power && Fun of scope with CSS variables
Jhey Tompkins
Jhey Tompkins

Posted on • Originally published at css-tricks.com

The Power && Fun of scope with CSS variables

You’re probably already at least a little familiar with CSS variables. If not, here’s a two-second overview: they are really called custom properties, you set them in declaration blocks like --size: 1em and use them as values like font-size: var(--size);, they differ from preprocessor variables (e.g. they cascade), and here’s a guide with way more information.

But are we using them to their full potential? Do we fall into old habits and overlook opportunities where variables could significantly reduce the amount of code we write?

This article was prompted by a recent tweet I made about using CSS variables to create dynamic animation behavior.

Let’s look at a couple of instances where CSS variables can be used to do some pretty cool things that we may not have considered.

Basic scoping wins

The simplest and likely most common example would be scoped colors. And what’s our favorite component to use with color? The button. 😅

Consider the standard setup of primary and secondary buttons. Let’s start with some basic markup that uses a BEM Syntax.

<button class="button button--primary">Primary</button>
<button class="button button--secondary">Secondary</button>
Enter fullscreen mode Exit fullscreen mode

Traditionally, we might do something like this to style them up:

.button {
  padding: 1rem 1.25rem;
  color: #fff;
  font-weight: bold;
  font-size: 1.25rem;
  margin: 4px;
  transition: background 0.1s ease;
}

.button--primary {
  background: hsl(233, 100%, 50%);
  outline-color: hsl(233, 100%, 80%);
}

.button--primary:hover {
  background: hsl(233, 100%, 40%);
}

.button--primary:active {
  background: hsl(233, 100%, 30%);
}

.button--secondary {
  background: hsl(200, 100%, 50%);
  outline-color: hsl(200, 100%, 80%);
}

.button--secondary:hover {
  background: hsl(200, 100%, 40%);
}

.button--secondary:active {
  background: hsl(200, 100%, 30%);
}
Enter fullscreen mode Exit fullscreen mode

That’s an awful lot of code for something not particularly complex. We haven’t added many styles and we’ve added a lot of rules to cater to the button’s different states and colors. We could significantly reduce the code with a scoped variable.

In our example, the only differing value between the two button variants is the hue. Let’s refactor that code a little then. We won’t change the markup but cleaning up the styles a little, we get this:

.button {
  padding: 1rem 1.25rem;
  color: #fff;
  font-weight: bold;
  font-size: 1.25rem;
  margin: 1rem;
  transition: background 0.1s ease;
  background: hsl(var(--hue), 100%, 50%);
  outline-color: hsl(var(--hue), 100%, 80%);

}

.button:hover {
  background: hsl(var(--hue), 100%, 40%);
}

.button:active {
  background: hsl(var(--hue), 100%, 30%);
}

.button--primary {
  --hue: 233;
}

.button--secondary {
  --hue: 200;
}
Enter fullscreen mode Exit fullscreen mode

This not only reduces the code but makes maintenance so much easier. Change the core button styles in one place and it will update all the variants! 🙌

I’d likely leave it there to make it easier for devs wanting to use those buttons. But, we could take it further. We could inline the variable on the actual element and remove the class declarations completely. 😲

<button class="button" style="--hue: 233;">Primary</button>
<button class="button" style="--hue: 200;">Secondary</button>
Enter fullscreen mode Exit fullscreen mode

Now we don’t need these 👍

.button--primary {
  --hue: 233;
}

.button--secondary {
  --hue: 200;
}
Enter fullscreen mode Exit fullscreen mode

Inlining those variables might not be best for your next design system or app but it does open up opportunities. Like, for example, if we had a button instance where we needed to override the color.

button.button.button--primary(style=`--hue: 20;`) Overridden
Enter fullscreen mode Exit fullscreen mode

Having fun with inline variables

Another opportunity is to have a little fun with it. This is a technique I use for many of the Pens I create over on CodePen. 😉

You may be writing straightforward HTML, but in many cases, you may be using a framework, like React or a preprocessor like Pug, to write your markup. These solutions allow you to leverage JavaScript to create random inline variables. For the following examples, I’ll be using Pug. Pug is an indentation-based HTML templating engine. If you aren’t familiar with Pug, do not fear! I’ll try to keep the markup simple.

Let’s start by randomizing the hue for our buttons:

button.button(style=`--hue: ${Math.random() * 360}`) First
Enter fullscreen mode Exit fullscreen mode

With Pug, we can use ES6 template literals to inline randomized CSS variables. 💪

Animation alterations

So, now that we have the opportunity to define random characteristics for an element, what else could we do? Well, one overlooked opportunity is animation. True, we can’t animate the variable itself, like this:

@keyframes grow {
  from { --scale: 1; }
  to   { --scale: 2; }
}
Enter fullscreen mode Exit fullscreen mode

But we can create dynamic animations based on scoped variables. We can change the behavior of animation on the fly! 🤩

Example 1: The excited button

Let’s create a button that floats along minding its own business and then gets excited when we hover over it.

Start with the markup:

button.button(style=`--hue: ${Math.random() * 360}`) Show me attention
Enter fullscreen mode Exit fullscreen mode

A simple floating animation may look like this:

@keyframes flow {
  0%, 100% {
    transform: translate(0, 0);
  }
  50% {
    transform: translate(0, -25%);
  }
}
Enter fullscreen mode Exit fullscreen mode

This will give us something like this:

I’ve added a little shadow as an extra but it’s not vital. 👍

Let’s make it so that our button gets excited when we hover over it. Now, we could simply change the animation being used to something like this:

.button:hover {
  animation: shake .1s infinite ease-in-out;
}

@keyframes shake {
  0%, 100% {
    transform: translate(0, 0) rotate(0deg);
  }
  25% {
    transform: translate(-1%, 3%) rotate(-2deg);
  }
  50% {
    transform: translate(1%, 2%) rotate(2deg);
  }
  75% {
    transform: translate(1%, -2%) rotate(-1deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

And it works:

But, we need to introduce another keyframes definition. What if we could merge the two animations into one? They aren’t too far off from each other in terms of structure.

We could try:

@keyframes flow-and-shake {
  0%, 100% {
    transform: translate(0, 0) rotate(0deg);
  }
  25%, 75% {
    transform: translate(0, -12.5%) rotate(0deg);
  }
  50% {
    transform: translate(0, -25%) rotate(0deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Although this works, we end up with an animation that isn’t quite as smooth because of the translation steps. So what else could we do? Let’s find a compromise by removing the steps at 25% and 75%.

@keyframes flow-and-shake {
  0%, 100% {
    transform: translate(0, 0) rotate(0deg);
  }
  50% {
    transform: translate(0, -25%) rotate(0deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

It works fine, as we expected, but here comes the trick: Let’s update our button with some variables.

.button {
  --y: -25;
  --x: 0;
  --rotation: 0;
  --speed: 2;
}
Enter fullscreen mode Exit fullscreen mode

Now let’s plug them into the animation definition, along with the button’s animation properties.

.button {
  animation-name: flow-and-shake;
  animation-duration: calc(var(--speed) * 1s);
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

@keyframes flow-and-shake {
  0%, 100% {
    transform: translate(calc(var(--x) * -1%), calc(var(--y) * -1%))
      rotate(calc(var(--rotation) * -1deg));
  }
  50% {
    transform: translate(calc(var(--x) * 1%), calc(var(--y) * 1%))
      rotate(calc(var(--rotation) * 1deg));
  }
}
Enter fullscreen mode Exit fullscreen mode

All is well. 👍

Let’s change those values when the button is hovered:

.button:hover {
  --speed: .1;
  --x: 1;
  --y: -1;
  --rotation: -1;
}
Enter fullscreen mode Exit fullscreen mode

Nice! Now our button has two different types of animations but defined via one set of keyframes 🤯

Let’s have a little more fun with it. If we take it a little further, we can make the button a little more playful and maybe stop animating altogether when it’s active 😅

Example 2: Bubbles

Now that we’ve gone through some different techniques for things we can do with the power of scope, let’s put it all together. We are going to create a randomly generated bubble scene that heavily leverages scoped CSS variables.

Let’s start by creating a bubble. A static bubble.

.bubble {
  background: radial-gradient(100% 115% at 25% 25%, #fff, transparent 33%),
    radial-gradient(15% 15% at 75% 75%, #80dfff, transparent),
    radial-gradient(100% 100% at 50% 25%, transparent, #66d9ff 98%);
  border: 1px solid #b3ecff;
  border-radius: 100%;
  height: 50px;
  width: 50px;
}
Enter fullscreen mode Exit fullscreen mode

Our basic bubble

We are using background with multiple values and a border to make the bubble effect — but it’s not very dynamic. We know the border-radius will always be the same. And we know the structure of the border and background will not change. But the values used within those properties and the other property values could all be random.

Let’s refactor the CSS to make use of variables:

.bubble {
  --size: 50;
  --hue: 195;
  --bubble-outline: hsl(var(--hue), 100%, 50%);
  --bubble-spot: hsl(var(--hue), 100%, 75%);
  --bubble-shade: hsl(var(--hue), 100%, 70%);
  background: radial-gradient(100% 115% at 25% 25%, #fff, transparent 33%),
    radial-gradient(15% 15% at 75% 75%, var(--bubble-spot), transparent),
    radial-gradient(100% 100% at 50% 25%, transparent, var(--bubble-shade) 98%);
  border: 1px solid var(--bubble-outline);
  border-radius: 100%;
  height: calc(var(--size) * 1px);
  width: calc(var(--size) * 1px);
}
Enter fullscreen mode Exit fullscreen mode

That’s a good start. 👍

Let’s add some more bubbles and leverage the inline scope to position them as well as size them. Since we are going to start randomizing more than one value, it’s handy to have a function to generate a random number in range for our markup.

- const randomInRange = (max, min) => Math.floor(Math.random() * (max - min + 1)) + min
Enter fullscreen mode Exit fullscreen mode

With Pug, we can utilize iteration to create a large set of bubbles:

- const baseHue = randomInRange(0, 360)
- const bubbleCount = 50
- let b = 0
while b < bubbleCount
  - const size = randomInRange(10, 50)
  - const x = randomInRange(0, 100)
  .bubble(style=`--x: ${x}; --size: ${size}; --hue: ${baseHue}`)
  - b++
Enter fullscreen mode Exit fullscreen mode

Updating our .bubble styling allows us to make use of the new inline variables.

.bubble {
  left: calc(var(--x) * 1%);
  position: absolute;
  transform: translate(-50%, 0);
}
Enter fullscreen mode Exit fullscreen mode

Giving us a random set of bubbles:

Let’s take it even further and animate those bubbles so they float from top to bottom and fade out.

.bubble {
  animation: float 5s infinite ease-in-out;
  top: 100%;
}

@keyframes float {
  from {
    opacity: 1;
    transform: translate(0, 0) scale(0);
  }
  to {
    opacity: 0;
    transform: translate(0, -100vh) scale(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

That’s pretty boring. They all do the same thing at the same time. So let’s randomize the speed, delay, end scale and distance each bubble is going to travel.

- const randomInRange = (max, min) => Math.floor(Math.random() * (max - min + 1)) + min
- const baseHue = randomInRange(0, 360)
- const bubbleCount = 50
- let b = 0
while b < bubbleCount
  - const size = randomInRange(10, 50)
  - const delay = randomInRange(1, 10)
  - const speed = randomInRange(2, 20)
  - const distance = randomInRange(25, 150)
  - const scale = randomInRange(100, 150) / 100
  - const x = randomInRange(0, 100)
  .bubble(style=`--x: ${x}; --size: ${size}; --hue: ${baseHue}; --distance: ${distance}; --speed: ${speed}; --delay: ${delay}; --scale: ${scale}`)
  - b++
Enter fullscreen mode Exit fullscreen mode

And now, let’s update our styles

.bubble {
  animation-name: float;
  animation-duration: calc(var(--speed) * 1s);
  animation-delay: calc(var(--delay) * -1s);
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}

@keyframes float {
  from {
    opacity: 1;
    transform: translate(-50%, 0) scale(0);
  }
  to {
    opacity: 0;
    transform: translate(-50%, calc(var(--distance) * -1vh)) scale(var(--scale));
  }
}
Enter fullscreen mode Exit fullscreen mode

And we will get this:

With around 50 lines of code, you can create a randomly generated animated scene by honing the power of the scope! 💪

That’s it!

We can create some pretty cool things with very little code by putting CSS variables to use and leveraging some little tricks.

I do hope this article has raised some awareness for the power of CSS variable scope and I do hope you will hone the power and pass it on 😎

All the demos in this article are available in this CodePen collection.


As always, any questions, please feel free to leave a response or tweet me 🐦! And say "Hey!" anyway 😎

Top comments (0)