Custom properties define a new value type in CSS that allows for the use of variables through the var() function.

One of the most commonly cited reasons for using CSS preprocessors like Sass or Less is the ability to use variables in your style sheets, because nobody likes to scour through thousands of lines of CSS just to change a hex code or a margin value. Even with search and replace, odds are we may end up missing one or two declarations. Using variables allows us to make the change in one place and be assured that change will propagate throughout our code.

The thing about CSS preprocessors is that they need to be compiled into CSS before the browser can recognize your code. Since they have to be compiled, these "variables" are essentially static, they cannot be updated dynamically.

CSS custom properties allow us to have true dynamic variables, that can be modified at run-time. This implies that as their values change, the browser will repaint as required. In addition, they are native to CSS, removing the need for compilation. We can also make use of standard CSS behavior like inheritance and cascade, something we could not with preprocessor variables.

Custom property value syntax

With CSS custom properties, developers can assign arbitrary values to a property with a name of their choice. Officially, a custom property is any valid identifier that starts with two dashes, for example --quux. They are case-sensitive, which means --quux and --QUUX do not refer to the same value.

This is a simple example of how to define and use a CSS custom property that we define as --primary-color.

:root {
  --primary-color: #0099cc;

h1 {
  color: var(--primary-color);

a {
  color: var(--primary-color);

Changing the value of --primary-color will change the value of the header and links as well.

Custom properties can be declared on any element in the document and are resolved as per normal inheritance and cascade rules. You can see that from the example below:

[playground_embed height="300px" width="100%" user="huijing" hash="5t8Q4Zjj" panels="result,html,css" codepanels=""]

var() function notation

The var() function is what makes CSS variables work, as it is how the browser will substitute and insert the assigned value as the value of a property. The syntax looks like this:

var() = var( <custom-property-name> [, <declaration-value> ]? )

It cannot be used as a property name, selector or anything other than a property value. The following are examples of invalid uses of the var() function:

/* Variables cannot be used as property names */
.baz {
  --side: padding-left;
  var(--side): 1em;

/* Variables cannot be used as part of a property */
.qux {
  --gap: 0.5;
  margin-left: var(--gap)em;

As a variable, it can be used in place of any value in any CSS property of an element. The first argument supplied is the name of the custom property to be substituted, and the second argument is the fallback value, used if the custom property specified is invalid.

p {
  margin: var(--margin, 1em 2em);

One thing to note is that var() functions are substituted at computed-value time. This means that if the result of the var() function is invalid during substitution, the CSS declaration itself is invalid at computed-value time.

Another gotcha was pointed out by Lea Verou in her talk at CSSConf. The url() function is the only function where CSS variables do not work properly due to its odd parsing behavior.

/* This does NOT work */
.element {
  --img: "sad";
  background: url("img/" var(--img) ".jpg") center / cover;

/* But this does */
.element {
  --img: url("img/cat.jpg");
  background: var(--img) center / cover;


Custom properties introduce an element of versatility to CSS that we never had before. For example, we can utilize them to make internationalization easier to maintain by separating out strings from where they are used.

:root:lang(en) {
  --external-link: "external link";

:root:lang(de) {
  --external-link: "Externer Link";

a[href^="http"]::after {
  content: " (" var(--external-link) ")";

Even though custom properties cannot be used as part of another property, they can be used within calc() functions as well to build up new values in a programmatic way.

:root {
  --spacing: 20; 

.cell {
  margin-bottom: calc(var(--spacing + 10px));    

Another advantage of using custom properties is that its dynamic nature allows us to have contextual styling. Let's say we have a standard button style as well as slight modifications depending on where the button appears.

Without custom properties, a common approach is to utilize the concept of descendant selectors for such contextual styling. But this results in an increased complexity when it comes to managing specificity and leads to maintenance issues as the project grows larger. Perhaps there will come a time when a button in the header needs to look another way.

.o-btn {
  background: #30abd5;
  border: 1px solid #30abd5;
  color: #fff;

.c-header .o-btn {
  background: transparent;
  border: 1px solid #237dac;
  color: #237dac;

Custom properties negate the specificity issue by not locking in the button component within the header to a specific style, but rather changing the context in which the button component exists. In the below example, the header has a set of values for button styles that its descendants can use, because of the cascade, but these values are very simple to change, if necessary.

.o-btn {
  background: var(--btn-bg, #30abd5);
  border: 1px solid var(--btn-border, #30abd5);
  color: var(--btn-txt, #fff);

.c-header {
  --btn-bg: transparent;
  --btn-border: #237dac;
  --btn-txt: #237dac;

Live Demo

The following demo demonstrate the theming example explained in the previous section.

[playground_embed height="500px" width="100%" user="huijing" hash="r8i3rhVH" panels="result,html,css" codepanels=""]

Custom properties allow CSS variables to be used in media queries, something that was not possible with preprocessor variables. In the following example, you can vary the spacing between elements, as well as utilize variables to adjust the number of columns in your layout. Open the demo in a new window and try resizing the result panel:

[playground_embed height="700px" width="100%" user="huijing" hash="L9HQse2F" panels="result,html,css" codepanels=""]

Browser Support

[caniuse feature="css-variables"]