A custom property is most commonly thought of as a variable in CSS.

.card {
  --spacing: 1.2rem;
  padding: var(--spacing);
  margin-bottom: var(--spacing);
}

Above, --spacing is the custom property with 1.2rem as the value and var(--spacing) is the variable in use.

Perhaps the most valuable reason to use them: not repeating yourself (DRY code). In the example above, I can change the value 1.2rem in one place and have it affect two things. This brings something programming languages do to CSS.

There is a good bit to know about custom properties, so let’s get into it.

Why care about CSS Custom Properties?

  1. They help DRY up your CSS. That is “Don’t Repeat Yourself.” Custom properties can make code easier to maintain because you can update one value and have it reflected in multiple places. Careful though, overdoing abstraction can make have the opposite effect and make code less understandable.
  2. They are particularly helpful for things like creating color themes on a website.
  3. They unlock interesting possibilities in CSS. In no small part because they cascade.
  4. The fact that they can be updated in JavaScript opens up even more interesting possibilities.

Naming custom properties

Custom properties must be within a selector and start with two dashes (--):

/* Nope, not within a selector */
--foo: 1;

body {
  /* No, 0 or 1 dash won't work */
  foo: 1;
  -foo: 1; 

  /* Yep! */
  --foo: 1;

  /* OK, but they're different properties */
  --FOO: 1;
  --Foo: 1;
  
  /* Totally fine */
  --mainColor: red;
  --main-color: red;

  /* Special characters are a no */
  [email protected]: red;
  --black&blue: black;
  --black^2: black;
}

Best to stick with letters, numbers, and dashes while making sure the custom property is defined inside of a valid selector.

Properties as properties

You can set the value of a custom property with another custom property:

html {
  --red: #a24e34;
  --green: #01f3e6;
  --yellow: #f0e765;

  --error: var(--red);
  --errorBorder: 1px dashed var(--red);
  --ok: var(--green);
  --warning: var(--yellow);
}

Some people like doing it this way because it allows the name of a custom property to be descriptive and then used in another property with a more functional name, again helping keep things DRY. It can even help make the functional names more readable and understandable.

Valid values for custom properties

Custom properties are surprisingly tolerant when it comes to the values they accept.

Here are some basic examples that you’d expect to work, and do.

body {
  --brand-color: #990000;
  --transparent-black: rgba(0, 0, 0, 0.5);
  
  --spacing: 0.66rem;
  --max-reading-length: 70ch;
  --brandAngle: 22deg;

  --visibility: hidden;
  --my-name: "Chris Coyier";
}

See that? They can be hex values, color functions, units of all kinds, and even strings of text.

But custom properties don’t have to be complete values like that. Let’s look at how useful it can be to break up valid CSS values into parts we can shove into custom properties.

Breaking up values

You can use custom properties to break up multi-part values.

Let’s imagine you’re using a color function, say rgba(). Each color channel value in there can be its own custom property. That opens up a ton of possibilities, like changing the alpha value for a specific use case, or perhaps creating color themes.

Splitting colors

Take HSL color, for example. We can split it up into parts, then very easily adjust the parts where we want. Maybe we’re working with the background color of a button. We can update specific parts of its HSL makeup when the button is hovered, in focus, or disabled, without declaring background on any of those states at all.

button {
  --h: 100;
  --s: 50%;
  --l: 50%;
  --a: 1;

  background: hsl(var(--h) var(--s) var(--l) / var(--a));
}
button:hover { /* Change the lightness on hover */
  --l: 75%;
}
button:focus { /* Change the saturation on focus */
  --s: 75%;
}
button[disabled] {  /* Make look disabled */
  --s: 0%;
  --a: 0.5;
}

By breaking apart values like that, we can control parts of them in a way we never could before. Just look at how we didn’t need to declare all of the HSL arguments to style the hover, focus and disabled state of a button. We simply overrode specific HSL values when we needed to. Pretty cool stuff!

Shadows

box-shadow doesn’t have a shorthand property for controlling the shadow’s spread on its own. But we could break out the box-shadow spread value and control it as a custom property (demo).

button {
  --spread: 5px;
  box-shadow: 0 0 20px var(--spread) black;
}
button:hover {
  --spread: 10px;
}

Gradients

There is no such thing as a background-gradient-angle (or the like) shorthand for gradients. With custom properties, we can change just change that part as if there was such a thing.

body {
  --angle: 180deg;
  background: linear-gradient(var(--angle), red, blue);
}
body.sideways {
  --angle: 90deg;
}

Comma-separated values (like backgrounds)

Any property that supports multiple comma-separated values might be a good candidate for splitting values too, since there is no such thing as targeting just one value of a comma-separated list and changing it alone.

/* Lots of backgrounds! */
background-image:
  url(./img/angles-top-left.svg),
  url(./img/angles-top-right.svg),
  url(./img/angles-bottom-right.svg),
  url(./img/angles-bottom-left.svg),
  url(./img/bonus-background.svg);

Say you wanted to remove just one of many multiple backgrounds at a media query. You could do that with custom properties like this, making it a trivial task to swap or override backgrounds.

body {
  --bg1: url(./img/angles-top-left.svg);
  --bg2: url(./img/angles-top-right.svg);
  --bg3: url(./img/angles-bottom-right.svg);
  --bg4: url(./img/angles-bottom-left.svg);
  --bg5: url(./img/bonus-background.svg);
  
  background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4);
}
@media (min-width: 1500px) {
  body {
    background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4), var(--bg5);
  }
}

Grids

We’re on a roll here, so we might as well do a few more examples. Like, hey, we can take the grid-template-columns property and abstract its values into custom properties to make a super flexible grid system:

.grid {
  display: grid;
  --edge: 10px;
  grid-template-columns: var(--edge) 1fr var(--edge);
}
@media (min-width: 1000px) {
  .grid {
     --edge: 15%;
   }
}

Transforms

CSS will soon get individual transforms but we can get it sooner with custom properties. The idea is to apply all the transforms an element might get up front, then control them individually as needed:

button {
  transform: var(--scale, scale(1)) var(--translate, translate(0));
}
button:active {
  --translate: translate(0, 2px);
}
button:hover {
  --scale: scale(0.9);
}

Concatenation of unit types

There are times when combining parts of values doesn’t work quite how you might hope. For example, you can’t make 24px by smashing 24 and px together. It can be done though, by multiplying the raw number by a number value with a unit.

body {
  --value: 24;
  --unit: px;
  
  /* Nope */
  font-size: var(--value) + var(--unit);
  
  /* Yep */
  font-size: calc(var(--value) * 1px);

  /* Yep */
  --pixel_converter: 1px;
  font-size: calc(var(--value) * var(--pixel_converter));
}

Using the cascade

The fact that custom properties use the cascade is one of the most useful things about them.

You’ve already seen it in action in many of the examples we’ve covered, but let’s put a point on it. Say we have a custom property set pretty “high up” (on the body), and then set again on a specific class. We use it on a specific component.

body {
  --background: white;
}
.sidebar {
  --background: gray;
}
.module {
  background: var(--background);
}

Then say we’ve got practical HTML like this:

<body> <!-- --background: white -->

  <main>
    <div class="module">
      I will have a white background.
    </div>
  <main>

  <aside class="sidebar"> <!-- --background: gray -->
    <div class="module">
      I will have a gray background.
    </div>
  </aside>

</body>
Three CSS rulesets, one for a body, sidebar and module. the background custom property is defined as white on body and gray on sidebar. The module calls the custom property and shows an orange arrow pointing to the custom property defined in the sidebar since it is the nearest ancestor.
For the second module, .sidebar is a closer ancestor than body, thus --background resolves to gray there, but white in other places.

The “module” in the sidebar has a gray background because custom properties (like many other CSS properties) inherit through the HTML structure. Each module takes the --background value from the nearest “ancestor” where it’s been defined in CSS.

So, we have one CSS declaration but it’s doing different things in different contexts, thanks to the cascade. That’s just cool.

This plays out in other ways:

button {
  --foo: Default;
}
button:hover {
  --foo: I win, when hovered;
  /* This is a more specific selector, so re-setting 
     custom properties here will override those in `button` */
}

Media queries don’t change specificity, but they often come later (or lower) in the CSS file than where the original selector sets a value, which also means a custom property will be overridden inside the media query:

body {
  --size: 16px;
  font-size: var(--size);
}
@media (max-width: 600px) {
  body {
    --size: 14px;
  } 
}

Media queries aren’t only for screen sizes. They can be used for things like accessibility preferences. For example, dark mode:

body {
  --bg-color: white; 
  --text-color: black;

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

/* If the user's preferred color scheme is dark */
@media screen and (prefers-color-scheme: dark) {
  body {
    --bg-color: black;
    --text-color: white;
  }
}

The :root thing

You’ll often see custom properties being set “at the root.” Here’s what that means:

:root {
  --color: red;
}

/* ...is largely the same as writing: */
html {
  --color: red;
}

/* ...except :root has higher specificity, so remember that! */

There is no particularly compelling reason to define custom properties like that. It’s just a way of setting custom properties as high up as they can go. If you like that, that’s totally fine. I find it somehow more normal-feeling to apply them to the html or body selectors when setting properties I intend to make available globally, or everywhere.

There is also no reason you need to set variables at this broad of a scope. It can be just as useful, and perhaps more readable and understandable, to set them right at the level you are going to use them (or fairly close in the DOM tree).

.module {
  --module-spacing: 1rem;
  --module-border-width: 2px;

  border: var(--module-border-width) solid black;
}

.module + .module {
  margin-top: var(--module-spacing);
}

Note that setting a custom property on the module itself means that property will no longer inherit from an ancestor (unless we set the value to inherit). Like other inherited properties, there are sometimes reasons to specify them in place (at the global level), and other times we want to inherit them from context (at the component level). Both are useful. What’s cool about custom properties is that we can define them in one place, inherit them behind the scenes and apply them somewhere completely different. We take control of the cascade!

Combining with !important

You can make an !important modifier within or outside of a variable.

.override-red {
  /* this works */
  --color: red !important;  
  color: var(--color);

  /* this works, too */
  --border: red;
  border: 1px solid var(--border) !important;
}

Applying !important to the --color variable, makes it difficult to override the value of the --color variable, but we can still ignore it by changing the color property. In the second example, our --border variable remains low-specificity (easy to override), but it’s hard to change how that value will be applied to the border itself.

Custom property fallbacks

The var() function is what allows for fallback values in custom properties.

Here we’re setting a scale() transform function to a custom property, but there is a comma-separated second value of 1.2. That 1.2 value will be used if --scale is not set.

.bigger {
  transform: scale(var(--scale, 1.2));
}

After the first comma, any additional commas are part of the fallback value. That allows us to create fallbacks with comma-separated values inside them. For example, we can have one variable fall back to an entire stack of fonts:

html {
  font-family: var(--fonts, Helvetica, Arial, sans-serif);
}

We can also provide a series of variable fallbacks (as many as we want), but we have to nest them for that to work:

.bigger {
  transform: scale(var(--scale, var(--second-fallback, 1.2));
}

If --scale is undefined, we try the --second-fallback. If that is also undefined, we finally fall back to 1.2.

Using calc() and custom properties

Even more power of custom properties is unlocked when we combine them with math!

This kind of thing is common:

main {
  --spacing: 2rem;
}

.module {
  padding: var(--spacing);
}

.module.tight {
  /* divide the amount of spacing in half */
  padding: calc(var(--spacing) / 2)); 
}

We could also use that to calculate the hue of a complementary color:

html {
  --brand-hue: 320deg;
  --brand-color: hsl(var(--brand-hue), 50%, 50%);
  --complement: hsl(calc(var(--brand-hue) + 180deg), 50%, 50%);
}

calc() can even be used with multiple custom properties:

.slider {
  width: calc(var(--number-of-boxes) * var(--width-of-box));
}

Deferring the calc()

It might look weird to see calculous-like math without a calc():

body {
  /* Valid, but the math isn't actually performed just yet ... */
  --font-size: var(--base-font-size) * var(--modifier);

  /* ... so this isn't going to work */
  font-size: var(--font-size);
}

The trick is that as long as you eventually put it in a calc() function, it works fine:

body {
  --base-font-size: 16px;
  --modifier: 2;
  --font-size: var(--base-font-size) * var(--modifier);

  /* The calc() is "deferred" down to here, which works */
  font-size: calc(var(--font-size));
}

This might be useful if you’re doing quite a bit of math on your variables, and the calc() wrapper becomes distracting or noisy in the code.

@property

The @property “at-rule” in CSS allows you to declare the type of a custom property, as well its as initial value and whether it inherits or not.

It’s sort of like you’re creating an actual CSS property and have the ability to define what it’s called, it’s syntax, how it interacts with the cascade, and its initial value.

@property --x {
  syntax: '<number>';
  inherits: false;
  initial-value: 42;
}
Valid Types
  • length
  • number
  • percentage
  • length-percentage
  • color
  • image
  • url
  • integer
  • angle
  • time
  • resolution
  • transform-list
  • transform-function
  • custom-ident (a custom identifier string)

This means that the browser knows what kind of value it is dealing with, rather than assuming everything is a string. That means you can animate things in ways you couldn’t otherwise.

For example, say you have a star-shaped icon that you want to spin around with @keyframes and rotate with a transform. So you do this:

.star {
  --r: 0deg;
  transform: rotate(var(--r));
  animation: spin 1s linear infinite;
}

@keyframes spin {
  100% {
    --r: 360deg;
  }
}

That actually won’t work, as the browser doesn’t know that 0deg and 360deg are valid angle values. You have to define them as an <angle> type with @property for that to work.

@property --r {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

.star {
  --r: 0deg;
  transform: rotate(var(--r));
  animation: spin 1s linear infinite;
}

@keyframes spin {
  100% {
    --r: 360deg;
  }
}
Demo

Commas in values

This can be a smidge confusing. Maybe not so much this:

html {
  --list: 1, 2, 3;
}

But below, you’ll need a sharp eye to realize the fallback value is actually 1.2, 2. The first comma separates the fallback, but all the rest is part of the value.

html {
  transform: scale(var(--scale, 1.2, 2));
}

Learn more about fallbacks above ⮑

Advanced usage

The Raven is a technique that emulates container queries using math and custom properties. Be prepared, this goes from 0-100 in complexity right out of the gate!

Demo

Resize this demo to see a grid of inline-block elements change number of columns from 4 to 3 to 1.

Here’s a few more favorite examples that show off advanced usage of custom properties:

The initial and whitespace trick

Think of @media queries and how when one thing changes (e.g. the width of the page) you can control multiple things. That’s kind of the idea with this trick. You change one custom property and control multiple things.

The trick is that the value of initial for a custom property will trigger a fallback, while an empty whitespace value will not. For the sake of explanation, it let’s define two globally-scoped custom properties, ON and OFF:

:root {
  --ON: initial;
  --OFF: ;
}

Say we have a “dark” variation class which sets a number of different properties. The default is --OFF, but can be flipped to --ON whenever:

.module {
  --dark: var(--OFF);
}

.dark { /* could be a media query or whatever */
  --dark: var(--ON);
}

Now you can use --dark to conditinally set values that apply only when you’ve flipped --dark to --ON. Demo:

Lea Verou has a great writeup that covers all of this.

Inline styles

It’s totally legit to set a custom property in HTML with an inline style.

<div style="--color: red;"></div>

That will, like any inline style, have a very high level of specificity.

This can be super useful for when the HTML might have access to some useful styling information that would be too weird/difficult to put into a static CSS file. A good example of that is maintaining the aspect ratio of an element:

<div style="--aspect-ratio: 16 / 9;"></div>

Now I can set up some CSS to make a box of that exact size wherever I need to. The full writeup on that is here, but here’s CSS that uses trickery like the ol’ padded box applied to a pseudo element which pushes the box to the desired size:

[style*="--aspect-ratio"] > :first-child {
  width: 100%;
}
[style*="--aspect-ratio"] > img {  
  height: auto;
} 
@supports (--custom: property) {
  [style*="--aspect-ratio"] {
    position: relative;
  }
  [style*="--aspect-ratio"]::before {
    content: "";
    display: block;
    padding-bottom: calc(100% / (var(--aspect-ratio)));
  }  
  [style*="--aspect-ratio"] > :first-child {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
  }  
}

But hey, these days, we have a native aspect-ratio property in CSS, so setting that in the inline style might make more sense going forward.

<div style="aspect-ratio: 16 / 9;"></div>

Hovers and pseudos

There is no way to apply a :hover style (or other pseudo classes/elements) with inline styles. That is, unless we get tricky with custom properties. Say we want custom hover colors on some boxes — we can pass that information in as a custom property:

<div style="--hover-color: red;"><div>
<div style="--hover-color: blue;"><div>
<div style="--hover-color: yellow;"><div>

Then use it in CSS which, of course, can style a link’s hover state:

div:hover {
  background-color: var(--hover-color);
}

/* And use in other pseudos! */
div:hover::after {
  content: "I am " attr(style);
  border-color: var(--hover-color);
}

Custom properties and JavaScript

JavaScript can set the value of a custom property.

element.style.setProperty('--x', value);

Here’s an example of a red square that is positioned with custom properties, and JavaScript updates those custom property values with the mouse position:

Typically you think of JavaScript passing values to CSS to use, which is probably 99% of usage here, but note that you can pass things from CSS to JavaScript as well. As we’ve seen, the value of a custom property can be fairly permissive. That means you could pass it a logical statement. For example:

html {
  --logic: if (x > 5) document.body.style.background = "blue";
}

Then grab that value and execute it in JavaScript:

const x = 10;

const logic = getComputedStyle(document.documentElement).getPropertyValue(
  "--logic"
);

eval(logic);

Custom properties are different than preprocessor variables

Say you’re already using Sass, Less, or Stylus. All those CSS preprocessors offer variables and it’s one of the main reasons to have them as part of your build process.

// Variable usage in Sass (SCSS)
$brandColor: red;

.marketing {
  color: $brandColor;
}

So, do you even need to bother with native CSS custom properties then? Yes, you should. Here’s why in a nutshell:

  • Native CSS custom properties are more powerful then preprocessor variables. Their integration with the cascade in the DOM is something that preprocessor variables will never be able to do.
  • Native CSS custom properties are dynamic. When they change (perhaps via JavaScript, or with a media query), the browser repaints what it needs to. Preprocessor variables resolve to a value when they’re compiled and stay at that value.
  • Going with a native feature is good for the longevity of your code. You don’t need to preprocess native CSS.

I cover this in much more detail in the article “What is the difference between CSS variables and preprocessor variables?”

To be totally fair, there are little things that preprocessor variables can do that are hard or impossible with custom properties. Say you wanted to strip the units off a value for example. You can do that in Sass but you’ll have a much harder time with custom properties in CSS alone.

Can you preprocess custom properties?

Kinda. You can do this, with Sass just to pick one popular preprocessor:

$brandColor: red;
body {
  --brandColor: $brandColor;
}

All that’s doing is moving a Sass variable to a custom property. That could be useful sometimes, but not terribly. Sass will just make --brandColor: red; there, not process the custom property away.

If a browser doesn’t support custom properties, that’s that. You can’t force a browser to do what custom properties do by CSS syntax transformations alone. There might be some kind of JavaScript polyfill that parses your CSS and replicates it, but I really don’t suggest that.

The PostCSS Custom Properties plugin, though, does do CSS syntax transforms to help. What it does is figure out the value to the best of it’s ability, and outputs that along with the custom property. So like:

:root {
  --brandColor: red;
}
body {
  color: var(--brandColor);
}

Will output like this:

:root {
  --brandColor: red;
}
body {
  color: red;
  color: var(--brandColor);
}

That means you get a value that hopefully doesn’t seem broken in browsers that lack custom property support, but does not support any of the fancy things you can do with custom properties and will not even attempt to try. I’m a bit dubious about how useful that is, but I think this is about the best you can do and I like the spirit of attempting to not break things in older browsers or newer browsers.

Availiability

Another thing that is worth noting about the difference between is that with a CSS preprocessor, the variables are available only as you’re processing. Something like $brandColor is meaningless in your HTML or JavaScript. But when you have custom properties in use, you can set inline styles that use those custom properties and they will work. Or you can use JavaScript to figure out their current values (in context), if needed.

Aside from some somewhat esoteric features of preprocessor variables (e.g. some math possibilities), custom properties are more capable and useful.

Custom properties and Web Components (Shadow DOM)

One of the most common and practical ways to style of Web Components (e.g. a <custom-component> with shadow DOM) is by using custom properties as styling hooks.

The main point of the shadow DOM is that it doesn’t “leak” styles in or out of it, offering style isolation in a way that nothing else offers, short of an <iframe>. Styles do still cascade their way inside, I just can’t select my way inside. This means custom properties will slide right in there.

Here’s an example:

Another common occurrence of the shadow DOM is with SVG and the <use> element.

Video: “CSS Custom Properties Penetrate the Shadow DOM”

Browser support

Desktop

Chrome Firefox IE Edge Safari
49 31 No 16 10

Mobile / Tablet

Android Chrome Android Firefox Android iOS Safari
90 87 90 10.0-10.2

You can preprocess for deeper browser support, with heavy limitations.

@supports

If you would like to write conditional CSS for when a browser supports custom properties or not:

@supports (--custom: property) {
  /* Isolated CSS for browsers that DOES support custom properties, assuming it DOES support @supports */
}

@supports not (--custom: property) {
  /* Isolated CSS for browsers that DON'T support custom properties, assuming it DOES support @supports */
}

Credit

Thanks to Miriam Suzanne for co-authoring this with me!



Source link

Write A Comment