Back to Blog

The CSS Files - Variables

Andrew Moscardino | May 26th, 2021

One of the biggest issues with writing large amounts of CSS is keeping things consistent. For example, in a large codebase a single color can be used hundreds of times in hundreds of places. This repetition makes maintenance difficult as a simple design tweak like changing a color can result in that change needing to be made in many, many places.

CSS pre-processors like Sass and Less attempt to solve this particular problem (and others) by including variables. Variables let you set common values like colors and sizes in a single place then reference the variable when you need to use those values. Now a simple color change only needs to be made in one place.

Modern CSS, though, is very powerful and CSS now has native support for variables. You don’t need any build tools or pipelines; they are just part of the language. The code you write is the code that the browser uses. Formally, variables are “custom properties” but are commonly referred to simply as variables. Let’s explore how they can be used and how they can make writing CSS a much better experience.

Declaring and Using Variables

One of the telling things about the name “custom properties” is that they are CSS properties. Since they are properties, they have to be declared within a CSS rule like any other property:

/* Nope! */
--brand-color: #003d7d;

.foo {
    /* Yep! */
    --brand-color: #003d7d;
}

To differentiate variables from standard properties, they must start with --. Any name can be used though, so long as you only use letters, numbers, and dashes. Remember that they are case-sensitive, so --foo and --Foo are not the same variable.

Variables are also part of the normal cascade of CSS properties. This effectively means that you will want to declare your global variables on the highest element in the document tree, which is almost always html. But if you have styles that apply to the html element and want to keep your variable declarations separate, a common practice is to use the :root pseudo-class:

:root {
    --brand-color: #003d7d;
}

The values of your variables can be many different things as basically any valid CSS value can be the value of a variable. Sizes and colors are just the beginning as entire border and background values can also be stored. You can also use the value from one variable to set another. The possibilities are endless.

:root {
    --brand-color: #003d7d;
    --spacing: 4px;
    --spacing-large: calc(var(--spacing) * 2);
    --border: 2px solid var(--brand-color);
    --info-icon-bg-image: url('data:/...');
}

In the code sample above, you probably noticed how we access the value of variables: the var() function. The first parameter to that function should be the variable you need the value from. The second (optional) parameter is used as a fallback value. This is useful if you are not sure if a variable has been set and can be used for all sorts of fun tricks.

:root {
    --some-color: #003d7d;
}

.widget {
    background-color: var(--some-color);
    font-size: var(--font-size, 16px);
}

In that example, since we did not declare a --font-size variable, var() will return 16px since that is the fallback value. Note that only the first value is considered the variable to evaluate and, like the values of variables, the fallback value can contain commas. For example, var(--foo, red, blue) defines a fallback of red, blue and not two separate fallback values.

Variable Scope

I mentioned earlier that variables are part of the normal cascade. This means that variables declared at the root scope are global and apply to everything on the page, but the cascade also enables two amazing features.

First, you can alter variables in smaller scopes. Think of having a global color that is used for all the buttons on a page. If you want to make all buttons within a specific kind of widget a different color, you can re-declare the variable on the widget class and any buttons that are within that widget will use the redefined color.

<style>
:root {
    --btn-color: #003d7d;
}

.widget {
    /* Redefine the value here so any buttons within a .widget will use this color */
    --btn-color: #00aeea;
}

.btn {
    color: var(--btn-color);
    border: 1px solid var(--btn-color);
}
</style>

<button class="btn">Normal Button</button>

<div class="widget">
    <button class="btn">Button with a different color</button>
</div>

The second feature this unlocks is being able to scope variables to specific components. If you set a variable like --widget-color on a .widget rule, then only elements that are descendants of a .widget element will be able to use the --widget-color variable. Trying to access the value outside of the scope in which it was declared will result in no value being used (not an error).

Advantages of Variables

Variables are a great addition to CSS. They have great potential to simplify large codebases by unifying disparate values. They can also simplify rules by allowing you to change specific values based on an element’s state or class rather than needing to re-declare entire properties. Because variables can be changed in various places including within @media queries, they make it easy to integrate system light/dark mode settings into your site. When combined with the powerful calc() function, you can turn a few simple variables into a comprehensive design system.

Are they a replacement for CSS pre-processors? No. Sass and Less still have many unique features that CSS lacks. They allow rules to be nested to reduce selector duplication. They have color functions which are difficult to replicate in CSS. There are still things they can do which the browser is simply not capable of, but they do require some sort of build process to generate the CSS that can be used.

All in all, pre-processors still have their place, but native variables expand the power of CSS without the need for any additional tooling.

Learn More