Proper way to use variables in CSS/SCSS

In April 2021 I wrote a blog post about Native CSS variables vs. SCSS variables. As everything changes fast in front end web development world, I no longer want to encourage people to use this “hybrid” strategy.

I found it cumbersome having to define things multiple times so using both SCSS variables and CSS variables got old very fast. What changed:

Use rgba() only if absolutely necessary

Don’t use colors with opacity that are not needed. It’s surprising how you can figure out simpler ways to achieve the same things. For example the background mask does not need to be defined as this:

.mask {
  background-color: rgba(0, 0, 0, .5);

Instead you can simply define black background with 50% opacity:

.mask {
  background-color: var(--color-black);
  opacity: .5;

So your vars are left intact. Rgba() colors are often only needed in some unique scenarios where you need to have more content like text inside a block that has a transparent background. Then it’s completely okay to use the RGB format directly in your variables like so:

// CSS variables for colors
:root {
  --color-background-overlay: rgba(0, 0, 0, .5);

.mask {
  background-color: var(--color-background-overlay);

Prefer original colors

As for regular text with opacity, prefer direct HEX colors. For example #000 with 50% opacity is #808080. Use that. Prefer the original value.

Same goes for darken() and lighten(). I find myself no longer using these helper functions, they make easy things too complicated. Instead I just name my colors like this in variables/_colors.scss:

// CSS variables for colors
:root {
  // Brand color pool
  --color-mine-shaft: #303030;
  --color-black: var(--color-mine-shaft);
  --color-black-rock: #2b303c;
  --color-blue-chalk: #ece6f2;
  --color-east-bay: #545773;
  --color-pattens-blue: #eaf1f8;
  --color-scorpion: #5f5f5f;
  --color-valhalla: #2a2d3e;
  --color-white: #fff;
  --color-grass-green: #2fc96d;
  --color-cobalt-blue: #603dff;
  --color-persian-blue: #3418b4;
  --color-cobalt-blue-light: #eeebff;
  --color-imperial-red: #ea5e6e;
  --color-mustard-yellow: #fcca3d;
  --color-beige: #f5f3ee;
  --color-dark-beige: #e4e2d7;
  --color-pitch: #000;
  --color-silver-chalice: #a9a9a9;

  // Main element colors
  --color-heading: var(--color-black);
  --color-main: var(--color-cobalt-blue);
  --color-paragraph: var(--color-black);

This makes things much easier. If you for some reason need color that is “darkened” 50%, you can just create a new variable for it. I repeat: Prefer 👏 original 👏 values.

Components use SCSS variables, but that’s okay

SCSS mixin components support CSS variables in some extent so it’s okay to use dollar-attributes as it only lives inside the mixin. For example for a default button component you just use it like you are used to:

.element-outside-the-default-scope > .button {
  @include button($color: var(--color-outside-the-scope));

Media queries come from SCSS variable breakpoints

It’s not both possible or reasonable to use CSS variables in media queries so just define your breakpoints as usual. I have learned that in 2020s you have breakpoints from 3 to 100 so you no longer need to define variables for every single breakpoint. Just do your styling per-element instead of per-screen (like it used to be back in the days when you had just a couple of screen sizes with breakpoints for mobile, iPad and desktop).

This per-element approach helps tremendously. Also always remember to use min-width first to have mobile first approach. If not possible, you can have multiple max-widths if needed.

Keep consistency in mind for ease of use and maintainability

It’s easy to get lost in fresh ways to do things. I remember when nesting was a brand new thing in SCSS and obviously felt somehow obligated to nest all the things just because we could. More sooner than later found myself completely lost in the CSS jungle of spaghetti code which was no longer reusable or maintainable. You could not found the start or the end of the elements. Same learning path seems to be with variables and other new fancy things – first when you just don’t know enough, you try and fail and try again. This is the best thing in front end development, this is the way you learn.

I hope this made some sense to the current updated workflow that I’m using. This could of course (as everything) change in the future.

Comment on Twitter