CSS variables (also know as CSS custom properties) can hold all sorts of things. Some of these things were not obvious to me, which is why I decided to write this.

To be clear, this article is focused on what you can put in a CSS variable — along with an animation demo near the end because I couldn’t help myself. If you’re looking for broader guidance on using CSS variables, there are other great articles for that.

Colorful bowls

Unit Values and a Little Math

Let’s start out easy. It’s common to put numerical values with units into a CSS variable. These are formally known as dimensions.

:root {
  --nice-padding: 20px;
  --decent-font-size: 1.25rem;
}

article {
  padding: var(--nice-padding);
  font-size: var(--decent-font-size);
}

Variables can hold the results of calculations involving other variables via calc().

:root {
  --image-width: 800px;

  /* calculate height to preserve a 4:3 aspect ratio */
  --image-height: calc(var(--image-width) / (4/3));
}

img {
  width: var(--image-width);
  height: var(--image-height);
}

Similarly, variables can be used with built-in CSS functions.

:root {
  --min: 1rem;
  --max: 4rem;
  --clamped-font-size: clamp(var(--min), 2.5vw, var(--max));
}

p {
  font-size: var(--clamped-font-size);
}

Unitless Numerical Values

Variables can also hold unitless numerical values. Some CSS properties use these values directly.

:root {
  --obnoxiously-big-number: 9001;
}

.important-modal {
  z-index: var(--obnoxiously-big-number);
}

But other times, you may want to apply units to these values. This can be done with multiplication in a calc() expression.

:root {
  --magic-number: 41;
}

.crazy-box {
  width: calc(var(--magic-number) * 1%);
  padding: calc(var(--magic-number) * 1px);
  transform: rotate(calc(var(--magic-number) * 1deg));
}

Non-numerical Things

CSS variables aren’t just for numbers. They can hold pre-defined keywords that properties recognize.

:root {
  --bullets: circle;
  --casing: uppercase;
}

ul {
  list-style-type: var(--bullets);
  text-transform: var(--casing);
}

There are also custom identifiers that point to things you’ve defined and named, like an animation-name or a grid-area as shown below.

:root {
  --layout-position: center-stage;
}

body {
  grid-template-areas: 'left center-stage right';
}

main {
  grid-area: var(--layout-position);
}

Content Strings

The ::before and ::after pseudo-elements use the content property to display, well, content. This content can be a couple different things, but it’ll often be text strings.

The following CSS shows content being populated with string variables. You can also see how to concatenate string variables with other strings and how to pull a string value from an attribute with attr().

:root {
  --open: '(';
  --close: ')';
  --heading-hint: ' ' var(--open) 'this is a heading' var(--close);
  --link-hint: ' ' var(--open) attr(href) var(--close);
}

h1::after {
  content: var(--heading-hint);
}

a::after {
  content: var(--link-hint);
}

Here’s a demo to show you the results.

See the Pen CSS Variables with Content Strings by Will Boyd (@lonekorean) on CodePen.

Images

CSS variables can also hold images. Images can also be displayed in content, but seeing them in background-image is probably more familiar.

:root {
  /* image from an external URL (PNG in this case) */
  --image-from-somewhere: url(https://codersblock.com/assets/images/logo.png);

  /* image from embedded data (SVG in this case) */
  --image-embedded: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M8 256c0 136.966 111.033 248 248 248s248-111.034 248-248S392.966 8 256 8 8 119.033 8 256zm248 184V72c101.705 0 184 82.311 184 184 0 101.705-82.311 184-184 184z'%3E%3C/path%3E%3C/svg%3E");
}

.a {
  background-image: var(--image-from-somewhere);
}

.b::after {
  content: var(--image-embedded);
}

This lets you tuck away long and unwieldy image URLs and just use shorter variable names where needed.

Shorthand

A CSS variable can be used as a single value in a shorthand, or as the entire shorthand itself. Both container elements below will have the same padding.

:root {
  --top-padding: 60px;
  --all-padding: 60px 20px 40px 10px;
}

.container {
  padding: var(--top-padding) 20px 40px 10px;
}

.another-container {
  padding: var(--all-padding);
}

What I found interesting is that CSS variables can also hold a partial segment of a shorthand that contains multiple values.

:root {
  --weight-and-size: bold 3rem;
}

body {
  font: var(--weight-and-size)/1.25 sans-serif;
}

Lists

Some properties, like background and box-shadow, can take a list of things. You can use a CSS variable as a single item in the list, a sublist of the list, or the entire list.

Here are a few examples of mixing box-shadow lists and CSS variables together. I also snuck in an example of putting a list of colors into a CSS variable to use in a linear-gradient(), because you can totally do that, too!

/*
  quick reminder of the anatomy of a box-shadow!
  box-shadow: <x-offset> <y-offset> <blur> <spread> <color>;
*/

:root {
  --single-shadow:
    0 0 0 40px #355c7d;
  --multi-shadow:
    0 0 0 60px #f67280,
    0 0 0 80px #6c5b7b;
  --gradient-colors: #f1bbba, #ece5ce, #c5e0dc;
}

.a {
  box-shadow:
    0 0 0 20px #60b99a,
    var(--single-shadow);
}

.b {
  box-shadow:
    var(--multi-shadow);
}

.c {
  box-shadow:
    0 0 0 20px #60b99a,
    var(--single-shadow),
    var(--multi-shadow);
}

body {
  background-image: linear-gradient(45deg, var(--gradient-colors));
}

And here’s a demo to see how things look.

See the Pen CSS Variables with Shadow Lists by Will Boyd (@lonekorean) on CodePen.

Colors

Can’t forget about colors! CSS variables work great for colors and it’s very common to see them used to define a color theme that is easy to consume throughout a site.

@media (prefers-color-scheme: light) {
  :root {
    --color-text: #233742;
    --color-links: #d80b77;
    --color-bg: white;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: white;
    --color-links: #b4cddd;
    --color-bg: #233742;
  }
}

body {
  color: var(--color-text);
  background-color: var(--color-bg);
}

a {
  color: var(--color-links);
}

You can also put separate color parameter values into CSS variables, then combine them into a color using rgb()/rgba() and hsl()/hsla(). Here’s an example using rgb().

:root {
  --red: 216;
  --green: 11;
  --blue: 119;
}

a {
  color: rgb(var(--red), var(--green), var(--blue));
}

Bringing It All Together with Animation

Can you put an animated value into a CSS variable? The answer is: it’s complicated!

By default, mostly no. Some browsers will show a single discrete jump from start value to end value. Some browsers will do nothing. Either way, you won’t get any sort of smooth, interpolated animation.

The problem is, your browser doesn’t know how to animate your made-up variables. But good news! You can solve this thanks to the Properties and Values API. It’s part of Houdini and currently works in Chrome, Edge, and Opera, with more browsers expected to hop onboard.

Here’s how it works. First, declare your custom properties.

@property --red {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0;
}
@property --green {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0;
}
@property --blue {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0;
}

This is you telling your browser “hey, here are some variables, they’re integers that default to 0”. Then your browser can say “oh integers, sweet, I know how to animate those”. And now we’re ready for animation.

@keyframes red-fade {
  50% { --red: 255; }
}
@keyframes green-fade {
  50% { --green: 255; }
}
@keyframes blue-fade {
  50% { --blue: 255; }
}

:root {
  animation: red-fade 16s, green-fade 14s, blue-fade 12s;
  animation-iteration-count: infinite;
}

In the above CSS, there’s a keyframe animation for each variable going from 0 (the initial-value we declared) to 255 and back again. These values are animated on the :root element and all child elements can use them because we declared the variables with inherits: true.

.swatch {
  background-color: rgb(var(--red), var(--green), var(--blue));
}

And with that, we have 3 independently animated CSS variables that are combined together via rgb() to create a constantly shifting background-color. No JavaScript. Neat!

See the Pen CSS Variables Color Animation by Will Boyd (@lonekorean) on CodePen.

There’s actually a lot going on in that demo. Multiple animations are being fueled by that trio of animated CSS variables (--red, --green, --blue). Let’s break it down.

First, the big RGB color swatch. We already talked through this one.

Second, the indicators moving up and down over the color bars. These are positioned by converting CSS variables to negative px values. For example, here’s the red one.

.red .indicator {
  transform: translateY(calc(var(--red) * -1px));
}

Third, the numbers displayed under the color bars. This one is interesting. As mentioned earlier, you can use content to display strings, but that won’t work with our integer variables. However, CSS counters work with integer values and can be displayed just like a string with content.

.red .value::before {
  /* in goes an integer variable */
  counter-reset: color-value var(--red); 

  /* out comes a string-like counter value */
  content: counter(color-value);
}

Basically, we’re abusing CSS counters to convert an integer to a string. Fun times.

CSS-Wide Values

Custom properties follow the same rules as built-in properties when it comes to cascade and inheritance. As such, you can use the same special CSS-wide values of initial, inherit, unset, and revert on CSS variables to control what is ultimately put in them.

Goodbye

From a syntax perspective, CSS variables are “extremely permissive”. There are definitely more types of things you can put in them that I haven’t specifically covered, but hopefully I’ve shown enough to give you some perspective of the possibilities. Plus we got to play around with animated colors, so that was fun.