CodyHouse Framework + Components are featured on Product Hunt! Join the discussion →

Why we prefer CSS Custom Properties to SASS variables

Some practical examples of how CSS variables can power-up your workflow.

Since the release of our framework a few months ago, we've been asked by many users why we opted for CSS variables, instead of SASS variables, even though we do use SASS in the framework. In this article, I'll go through the advantages of using custom properties and why they've become crucial in our workflow.

Content:

  1. Creating and applying color themes
  2. Controlling the type scale
  3. Controlling the spacing scale
  4. Editing vertical rhythm on a component level
  5. Abstracting components behavior
  6. What's the catch?
  7. Can I use CSS variables with a preprocessor?
  8. Conclusion

👋 First time you hear about the CodyHouse Framework?

Defining variables #

In this article I'm assuming you're familiar with the basics of both CSS custom properties and SASS (or any other CSS preprocessor). If you're not, let's start from a basic example:

In SCSS:

$color-primary: hsl(220, 90%, 56%);

.link {
  color: $color-primary;
}

In CSS:

:root {
  --color-primary: hsl(220, 90%, 56%);
}

.link {
  color: var(--color-primary);
}

Native, custom properties allow you to define variables without the need for CSS extensions (i.e., SASS).

Are they the same? Not really! Unlike SASS variables, custom properties 1) are scoped to the element they are declared on, 2) cascade and 3) can be manipulated in JavaScript. These three features open a whole new world of possibilities. Let me show you some practical examples!

1. Creating and applying color themes #

Here's an example of how you would create two (simplified) color themes using SASS variables:

$color-primary: blue;
$color-text: black;
$color-bg: white;
/* invert */
$color-primary-invert: red;
$color-text-invert: white;
$color-bg-invert: black;

.component {
  color: $color-text;
  background-color: $color-bg;

  a {
    color: $color-primary;
  }
}

.component--dark {
  color: $color-text-invert;
  background-color: $color-bg-invert;

  a {
    color: $color-primary-invert;
  }
}

In the example above, we have a 'default' theme, and a 'dark' theme where we invert the colors of background and text. Note that in the dark theme we need to go through each property where the color variables were used, and update them with a new variable.

As long as we stick to simplified (non-realistic) examples, no issue arises. What if we have a component with plenty of elements? Once again, we would be forced to rewrite all the properties where the color variables are used and replace the variables. And if you change the main component, you have to double check all the modifiers. So yeah...not so handy!

While building our framework, we came up with a different approach based on CSS variables. First of all, let's define the color variables:

:root, [data-theme="default"] {
  --color-primary: blue;
  /* color contrasts */
  --color-bg: white;
  --color-contrast-lower: hsl(0, 0%, 95%);
  --color-contrast-low: hsl(240, 1%, 83%);
  --color-contrast-medium: hsl(240, 1%, 48%);
  --color-contrast-high: hsl(240, 4%, 20%);
  --color-contrast-higher: black;
}

[data-theme] {
  background-color: var(--color-bg);
  color: var(--color-contrast-high);
}

[data-theme="dark"] {
  --color-primary: red;
  /* color contrasts */
  --color-bg: black;
  --color-contrast-lower: hsl(240, 6%, 15%);
  --color-contrast-low: hsl(252, 4%, 25%);
  --color-contrast-medium: hsl(240, 1%, 57%);
  --color-contrast-high: hsl(0, 0%, 89%);
  --color-contrast-higher: white;
}

FYI: in the example above we use data-* attributes to apply a color theme, but this has nothing to do with CSS variables vs. SASS variables. Also, we defined a scale of neutral values using a nomenclature based on the 'contrast level'.

The important point is that we don't need to create new color variables for our second (dark) theme. Unlike SASS, we can override the value of existing custom properties.

Here's how to apply the color variables to a component:

.component {
  color: var(--color-contrast-higher);
  background-color: var(--color-bg);
  border-bottom: 1px solid var(--color-contrast-low);

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

What about the dark variation of the component? We don't need additional CSS. Because we're overriding and not replacing variables, we only need to apply the correct color variables when we create the component for the first time. It doesn't matter how complicated the component becomes, once you've set the color themes in your _colors.scss file, and applied the color variables to the elements of your components, you can apply color themes in a very simple way:

<section data-theme="dark">
  <div class="component">
    <div class="child" data-theme="default"></div>
  </div>
</section>

In the example above, we've applied the 'dark' color theme to the section, and the 'default' color theme to the .child element. That's right, you can nest color themes!

This technique, made possible by the use of CSS custom properties, allows you to do in no time cool stuff like this. 👇

Creating color themes using the CodyHouse Framework

Here are some links in case you want to learn more about how to manage colors using the CodyHouse framework:

2. Controlling the type scale #

A type (or modular) scale is a set of harmonious (size) values that are applied to typography elements. Here's how you can set a type scale in SCSS using SASS variables:

$text-xs: 0.694em;
$text-sm: 0.833em;
$text-base-size: 1em;
$text-md: 1.2em;
$text-lg: 1.44em;
$text-xl: 1.728em;

A standard approach would be creating the type scale using a third-party tool (or doing the math), then importing the values into your style like in the example above.

While building our framework, we decided to incorporate the whole scale formula into the custom-style/_typography.scss file. Here's how we set the type scale using CSS variables:

:root {
  // body font size
  --text-base-size: 1em;
  
  // type scale
  --text-scale-ratio: 1.2;
  --text-xs: calc((1em / var(--text-scale-ratio)) / var(--text-scale-ratio));
  --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
  --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
  --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
  --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
  --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
  --text-xxxl: calc(var(--text-xxl) * var(--text-scale-ratio));
}

What's the advantage of such an approach? It gives you the possibility to control the whole typography system by editing only two variables: the --text-base-size (body font size) and the --text-scale-ratio (the scale multiplier).

'Yes, but can't you do the same using SASS variables'? No, if you want to modify your typography at specific breakpoints:

:root {
  @include breakpoint(md) {
    --text-base-size: 1.25em;
    --text-scale-ratio: 1.25;
  }
}

The snippet above is the cornerstone of our responsive approach. Because we use Ems relative units, when the --text-base-size (body font size) is modified, both typography and spacing are affected. You end up with a system that resizes all your components with almost no need to set media queries on a component level.

Edit Typography using the CodyHouse Framework

Here are some useful links on the topic:

3. Controlling the spacing scale #

The spacing scale is the equivalent of the type scale but applied to space values. Once again, including the scale formula into the framework allowed us to control the spacing system and make it responsive:

:root {
    --space-unit:  1em;
    --space-xxxxs: calc(0.125 * var(--space-unit)); 
    --space-xxxs:  calc(0.25 * var(--space-unit));
    --space-xxs:   calc(0.375 * var(--space-unit));
    --space-xs:    calc(0.5 * var(--space-unit));
    --space-sm:    calc(0.75 * var(--space-unit));
    --space-md:    calc(1.25 * var(--space-unit));
    --space-lg:    calc(2 * var(--space-unit));
    --space-xl:    calc(3.25 * var(--space-unit));
    --space-xxl:   calc(5.25 * var(--space-unit));
    --space-xxxl:  calc(8.5 * var(--space-unit));
    --space-xxxxl: calc(13.75 * var(--space-unit));
}

@supports(--css: variables) {
  :root {
    @include breakpoint(md) {
      --space-unit:  1.25em;
    }
  }
}

This approach becomes particularly powerful when combined with the typography method discussed in the previous chapter. With just a few lines of CSS, you end up with responsive components:

Making spacing responsive

One thing I love about using Ems units along with this spacing system is that if spacing and typography sizes look right at a specific breakpoint, they almost certainly look right at all breakpoints, regardless the fact that you update the --space-unit value. A corollary to that is I can design with nearly no need to resize the browser window (except when I want to change the behavior of a component); and when I do resize the window, spacing and typography adapt gracefully.

More links on the topic:

4. Editing vertical rhythm on a component level #

Unlike SASS variables, we can override the value of CSS variables. One way to take advantage of this feature is injecting custom properties into other custom properties, thus creating 'controls' that can be edited on a component level.

Here's an example: when you set the vertical spacing of a text component, you probably want to specify line-height and margin-bottom for your elements:

.article {
  h1, h2, h3, h4 {
    line-height: 1.2;
    margin-bottom: $space-xs;
  }

  ul, ol, p, blockquote {
    line-height: 1.5;
    margin-bottom: $space-md;
  }
}

This spacing, however, varies according to where this text is used. For example, if you want your text to be more condensed, you need to create a component variation where you apply different spacing values:

.article--sm {
  h1, h2, h3, h4 {
    line-height: 1.1;
    margin-bottom: $space-xxxs;
  }

  ul, ol, p, blockquote {
    line-height: 1.4;
    margin-bottom: $space-sm;
  }
}

...and so on anytime you wish to update vertical rhythm.

Here's an alternative approach based on CSS variables:

.text-component {
  --component-body-line-height: calc(var(--body-line-height) * var(--line-height-multiplier, 1));
  --component-heading-line-height: calc(var(--heading-line-height) * var(--line-height-multiplier, 1));
  --line-height-multiplier: 1;
  --text-vspace-multiplier: 1;

  h1, h2, h3, h4 {
    line-height: var(--component-heading-line-height);
    margin-bottom: calc(var(--space-xxxs) * var(--text-vspace-multiplier));
  }

  h2, h3, h4 {
    margin-top: calc(var(--space-sm) * var(--text-vspace-multiplier));
  }

  p, blockquote, ul li, ol li {
    line-height: var(--component-body-line-height);
  }
  
  ul, ol, p, blockquote, .text-component__block, .text-component__img {
    margin-bottom: calc(var(--space-sm) * var(--text-vspace-multiplier));
  }
}

The --line-height-multiplier and --text-vspace-multiplier are the two scoped controls of the text-component. When we create a modifier of the .text-component class, to edit vertical spacing we only need to override those two variables:

.article.text-component { // e.g., blog posts
  --line-height-multiplier: 1.13; // increase article line-height
  --text-vspace-multiplier: 1.2; // increase vertical spacing
}

In case you want to take this for a spin:

5. Abstracting components behaviour #

The possibility to override the value of a component can be used in many ways. In general, anytime you can abstract the behavior of a component in one or more variables, you're making your life easier when that component needs editing (or you have to create a variation of the component).

An example is our Auto Sized Grid component, where we use CSS grid to create a layout where the gallery items auto-fill the available space based on a min-width set in CSS, then we abstract the min-width value of the items, storing it in a variable.

That min-width value is the only thing you need to modify when you create a variation of the Auto Sized Grid component.

6. What's the catch? #

In two words: browser support. Hold on, though! You can use CSS custom properties in all the ways described in this article with the help of a PostCSS plugin. There are some limitations in the things you can do, and some changes (e.g., editing vertical rhythm) only apply to modern browsers. In these specific cases, you're free to use CSS variables as long as you're not disrupting the experience in older browsers. Check out our documentation for more info about the limitations of using CSS Variables today.

7. Can I use CSS variables with a preprocessor? #

Yes! As long as SASS (or any other preprocessor) allows you to do stuff you can't do in CSS, and you need that stuff, why not using it? SASS is not a library users have to download when they access your website. It's a tool in your workflow. We use SASS, for example, to define color functions that work with CSS variables.

8. Conclusion #

In this article, we've gone through a few examples that demonstrate what's the advantage of using CSS custom properties over SASS variables. We've focused on how they enable you to create 'controls' that speed up the way you modify components, or set rules that affect typography and spacing. We've covered a lot of ground, and I hope you can take something from this post and include it in your work. 😊

Would you like to share how you're using CSS variables, or do you have feedback on the article? Get in touch on Twitter!

Project duplicated.