← ← ← 5/2/2022, 4:55:20 PM | Posted by: Felippe Regazio
If you dont know about CSS Custom Properties yet, you really should learn about. I particularly prefer to use CSS custom properties instead of SASS variables always when possible, mostly because of its reactivity. For this post I'll assume that you're already comfortable with CSS Custom Properties and SASS Vars (with SCSS Syntax).
Sometimes the use of CSS Custom Properties is not a case of taste, but a necessity. It just fits perfectly to build themes, dynamic components, configurable pages etc. But as our page grows, also grows the maintenance complexity of the code base and the risk of create a modules labyrinth.
A common approach when dealing with SASS/SCSS variables is to create a _vars file which will hold most of our app variables, and in the case of CSS Custom Properties, also add a prefix to avoid conflicts. The idea here is the use of CSS Custom Props, but i think it fits to SASS vars as well. Here is a common pattern:
$prefix: pf;
:root {
--#{$prefix}-primary-color: #000000;
// this will compile to: --pf-primary-color: #000000;
}
Now we use it along the stylesheets:
@import './config/_vars';
.btn-example {
background-color: var(--#{$prefix}-primary-color);
}
Nice, you are separating concerns, using a prefix to avoid conflicts, maybe adding the _vars on a main file to keep your variables always available along your stylesheets. Nice, but there are some serious problems with this approach:
I'm not saying that this approach don't fits at all. But for scalable and large applications the above architecture can become a problem very quickly, or in the best scenario it will unnecessarily increase your css files size due code repetition. So, here is my 2 cents on that subject:
First of all, define getters and setters for your CSS Custom Properties. Then use your getters and setters to deal with variables along the app providing a pattern and reliability, now you can really separate the concerns in a solid architecture. Lets see how to do it:
Lets start adding a @mixin to declare a set of CSS Custom Properties. This mixin will be used to prefix our variables and add them on the code. It will also make our app really scalable and easy to maintain. If you need to modify how variables work on your app you dont need to open all the files to make changes then keep checking if something has broken, you just need to tune that mixin. Its also friendly for new devs by avoiding errors when declaring properties (syntax errors, forgetting the prefix, etc):
/**
* Use this mixin to declare a set of CSS Custom Properties.
* The variables in $css_variables will be properly prefixed.
* The use of this mixin is encoraged to keep a good scalability.
*
* Usage:
*
* @include cssvars((
* base-font-size: 65.5%,
* font-family: #{"HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif},
*
* primary-color: #33b5e5,
* secondary-color: #ff500a,
* ));
*
* Will result in
*
* root {
* --prefix-var-name: value;
* --prefix-var-name: value;
* --prefix-var-name: value;
* }
*
*/
@mixin cssvars($css_variables, $prefix: pf) {
:root {
@each $name, $value in $css_variables {
--#{$prefix}-#{$name}: #{$value};
}
}
}
Now instead of doing and maintain this:
:root {
--#{$prefix}-base-font-size: 65.5%;
--#{$prefix}-font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
--#{$prefix}-primary-color: #33b5e5;
--#{$prefix}-secondary-color: #ff500a;
}
You will do and maintain this:
@include cssvars((
base-font-size: 65.5%,
font-family: #{"HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif},
primary-color: #33b5e5,
secondary-color: #ff500a,
));
The result will be the same:
:root {
--pf-base-font-size: 65.5%;
--pf-font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
--pf-primary-color: #33b5e5;
--pf-secondary-color: #ff500a;
}
You can also override the prefix passing another one as the second argument. Note that for all the functions and mixins showed here, the $prefix will appear as param with a default value. I prefer to use it that way, but you can put it out and use a single $prefix var if you prefer.
Maybe you can add a new argument to override the ":root" selector creating scoped custom properties declarations, i dont know... You can tweak this mixin to fit your style and needings, the idea here is to have a setter for your Custom Properties that will allow you to have a pattern and single source of truth when dealing with your variables, leading to a solid application growth.
Lets create a getter to retrieve our CSS Custom Properties avoiding weird syntax and dense code for simple things:
/**
* Retrieve a css variable value with prefix
*
* Usage
*
* .selector {
* color: cssva(primary-color);
* }
*
* Will result in
*
* .selector {
* color: var(--prefix-primary-color);
* }
*/
@function cssvar($name, $prefix: pm) {
@return var(--#{$prefix}-#{$name});
}
Now, instead of this:
:root {
--#{$prefix}-button-height: 40px;
--#{$prefix}-button-color: #ffffff;
--#{$prefix}-button-background: #000000;
.button-primary {
height: var(--#{$prefix}-button-height);
line-height: var(--#{$prefix}-button-height);
color: var(--#{$prefix}-button-color);
background-color: var(--#{$prefix}-button-background);
}
}
We get this:
@include cssvars((
button-height: 40px,
button-color: #ffffff,
button-background: #000000,
));
.btn-primary {
height: cssvar(button-height);
line-height: cssvar(button-height);
color: cssvar(button-color);
background-color: cssvar(button-background);
}
Which will result in this:
:root {
--pm-button-height: 40px;
--pm-button-color: #ffffff;
--pm-button-background: #000000;
}
.btn-primary {
height: var(--pm-button-height);
line-height: var(--pm-button-height);
color: var(--pm-button-color);
background-color: var(--pm-button-height);
}
The syntax, readability and maintainment where improved. Also be aware that when we talk about prefix here, we are talking about prefixes for variable names like --prefix-color:blue, not css native properties like -moz-*.
Lets say we want to add a big button now. Simple, we just override the value of our button-height var to a bigger one in a class. So, we will need a mixin that updates (overrides) a custom property value. To do it in a clean way, add this mixin:
@mixin cssvar ($name, $value: '', $prefix: pm) {
--#{$prefix}-#{$name}: #{$value};
}
Now, instead of this:
@include cssvars((...));
.btn-primary {
height: cssvar(button-height);
line-height: cssvar(button-height);
color: cssvar(button-color);
background-color: cssvar(button-background);
&--big {
// ** LOOK HERE **
--#{$prefix}-button-height: 56px;
}
}
We get this:
@include cssvars((...));
.btn-primary {
height: cssvar(button-height);
line-height: cssvar(button-height);
color: cssvar(button-color);
background-color: cssvar(button-background);
&--big {
// ** LOOK HERE **
@include cssvar(button-height, 56px);
}
}
With the following result:
:root { ...vars }
.btn-primary {
height: var(--pm-button-height);
line-height: var(--pm-button-height);
color: var(--pm-button-color);
background-color: var(--pm-button-height);
}
.btn-primary--big {
--pm-button-height: 56px;
}
Now we have a powerful toolset :)
Now that we have a cool set of helpers, lets use it! Here is my tip:
At the end you will have a folder/file tree almost like that:
styles
config
_helpers.scss
_config.scss
modules
_buttons.scss
_form.scss
_typography.scss
_tables.scss
main.scss
At runtime, as we are talking about CSS Custom Properties, once declared in the :root {}, all variables will be globally available in its context. You can use them anywhere without myriads of @imports. They are just available. Of course this is sometimes a bad thing depending on your codebase size and can harm your performance - or if you dont need dynamic vars you can use SASS vars, but keeping them in its right place. What i mean is that you can easily tweak this architecture to scope your custom props.
Our main.scss would be something almost like that:
@import './config/_helpers';
@import './config/_config';
@import './modules/_typography';
@import './modules/_buttons';
@import './modules/_form';
@import './modules/_tables';
// etc
And this would be a module:
@include cssvars((
btn-primary-text-color: #ffffff,
btn-secondary-text-color: #ffffff,
btn-border-radius: 4px,
btn-text-transform: uppercase,
btn-font-size: 12px,
btn-font-weight: 600,
btn-height: 40px,
btn-padding: 0 30px,
));
.button, button,
input[type="button"],
input[type="submit"],
input[type="reset"] {
// { hard coded properties }
height: cssvar(btn-height);
padding: cssvar(btn-padding);
line-height: cssvar(btn-height);
font-size: cssvar(btn-font-size);
font-weight: cssvar(btn-font-weight);
letter-spacing: cssvar(btn-font-weight);
color: cssvar(primary-color);
border-radius: cssvar(btn-border-radius);
border: 1px solid cssvar(primary-color);
text-transform: cssvar(btn-text-transform);
&.focus, &:focus,
&.hover, &:hover {
// { ... whatever you want }
}
&.button-block {
display: block;
width: 100%;
}
&.button-primary {
color: cssvar(btn-primary-text-color);
background-color: cssvar(primary-color);
border-color: cssvar(primary-color);
}
&.button-secondary {
color: cssvar(btn-secondary-text-color);
background-color: cssvar(secondary-color);
border-color: cssvar(secondary-color);
}
&.button--big {
@include cssvar(btn-height, 56px);
}
}
This is just an example. A button can be far more complex. But note that all the things the button needs is in itself. And when imported, its Custom Properties will be globally available on the context. Also look that we are using our helpers to provide a pattern and reliability.
At the beginning of this post i talked about 6 common problems in SCSS code bases. Here is how that approach will solve them:
Now that we have a nice and clean syntax to deal with prefixed props there is no need to copy paste codes to gain speed, its easy and fast to use our helpers, change and maintain our css props is really fun and they are also prefixed and standardized.
Its easy and safe to onboard new devs on the code base. When talking about vars we basically have 3 functions to deal with variables and to explain to them. The _config file holds the app globals and the another vars are on the module itself:
// setter
@include cssvars((...));
// updater
@include cssvar(name, value);
// getter fn
prop: cssvar(name);
There is a real separation of concerns. We keep any module as a single universe, declaring everything there and only providing the output. If we need to split a module, it will become a folder with a main exporter file, no need to keep fishing files on the project trying to discover where are all the peaces.
The function and mixins showed here are a demonstration of how to provide an architecture to a solid growth. But there is no silver bullet, you can of course change it to fit your needings, change names, arguments, its just a spark of an idea, you can adapt as you need, the idea is help application to expand without pain :)