Moving from Node Sass and main.css to Dart Sass and scoped CSS components

15 minute read



TL;DR: Thinking about proper CSS architecture was already quite the science, but how do you approach this when you; A: start using the newly scoped syntax of Dart Sass and B: break up your CSS into CSS modules to be inserted into React components? What does this mean for style cascading? Folder structure? I try to muse aloud on these subjects.

I’m lucky. I was lucky enough to be able to use the internet for the first time at age 11. I distinctly remember searching for - and finding - information on wolves. I remember watching an image of a black wolf draw on the page line by line, my excitement barely contained at this magic happening on the screen.

I was lucky again fifteen years later, when I started my first development job as a front-ender. I was surrounded by a team that knew a lot more about this stuff than I did. I remember one of my first assignments was to figure out a CSS grid system called Susy by Miriam Suzanne.

To uncover my age to you gentle readers willing to do the math; it’s now another five years later. I find myself lucky yet again. A few months ago I first started learning React, right around the time Sass announced their new modular scss system based on Dart. What a joy!

I don’t particularly like having CSS inside my JSX files, so currently I import styling via external scss partials into my JS components. A styled component based javascript stack, coupled with a modular and scoped SCSS system. What more could a girl want? Well, some documentation, that’s what. And because the internet is not flooded with information about Dart Sass just yet (Miriam Suzanne’s posts on Sitepoint and CSS Tricks being the notable exceptions, ironically), I figured I’d help start remedy that.

So, what am I talking about exactly?

ITCSS

As a purebred chaotic, having structured code is very important to me. I need to know where to find which code, and I need to do it fast. SCSS was already revolutionary in letting me break up my CSS into structured files and folders and harnessing the power of variables, mixins and functions. I was a particular fan of working with an Inverted Triangle CSS (ITCSS)-like structure.

This structure emphasises the need to declare your settings and most broadly reaching styles first and working down into more detailed and complex components the further you go along. My similar structure, defined with those same colleagues who helped me start out as a developer, looked like this:

//====== 01 Settings: Core Variables of the project ( breakpoints, colours, font-sizes, etc )
@import '01-settings/variables';

//====== 02 Tools: Collection of all the globally used mixins and functions
@import '02-tools/functions';
@import '02-tools/mixins';

//====== 03 Generic: Base elements like CSS reset, @font-face. This is where the first actually outputted CSS lives.
@import '03-generic/reset';
@import '03-generic/font-face';

//====== 04 Autogenerated: Placeholder for possible auto-generated content (such as sprite sheet parameters, in ye old early days)
@import '04-auto-generated/iconmap';

//====== 05 Components: All reusable modular CSS elements like buttons, inputs, light boxes, et cetera. This would be the largest folder by far, the lists’s partials growing in complexity going down.
@import '05-components/typography';
@import '05-components/icons';
@import '05-components/buttons';
@import '05-components/modals';
... etc

//====== 06 Layout: Styles regarding specific pages or big content blocks that require a combination of components and unique layouts
@import '06-layout/header';
@import '06-layout/footer';
@import '06-layout/homepage';
... etc

//====== 07 Helpers: Helper classes to clear, show/hide elements in markup et cetera.
@import '07-helpers/helpers';

Declare your main variables first, globally used mixins and functions second, resets third, possible vendor css fourth, building blocks fifth, layouts sixth and finally possible helper classes seventh. Following this pattern allowed me to work bottom up, declaring the most basic styles first and adding complexity as I went along. Confusion about why some styles overrode others were a rarity, because I was basically following the natural cascade built into css.

Dart Sass

Then came Dart Sass. Formerly, I used Node Sass (a C/C++ port of LibSass). Dart Sass is the Sass developers’ new baby and includes a Javascript API that is compatible with Node Sass. It allows cross-platform installation without any pesky installation errors. Working in teams that develop in a range from Linux to Windows and OS X, these errors could sometimes put a real time drain on initial setups. Those problems are gone.

In practice, that’s the small change. The big change is the way imports are handled. Where the old version of sass copied the CSS native @import syntax, the new version stimulates the use of @use and @forward instead. @import still works, but @use and @forward mean your styles are now namespaced. I won’t go into extreme detail on this subject as Suzanne’s articles already cover that very well, but I do need to explain a bit of it to highlight its importance in code architecture.

If you were to @use a partial file in another file, it’s variables, functions and mixins become available in that second partial, and that second partial alone. It also becomes namespaced by default.

For instance we can now do this:

smurf.scss:

$body: #0ff;
$hat: #fff;

.smurf {
  background: $body
}

.smurf-hat {
  background: $hat
}

papa-smurf.scss:

@use smurf;
 // declaring this same variable name causes no conflicts or errors!
$hat: #f00;

.papa-smurf {
  // by prefacing the variable with the imported file from @use, allows us to use that variable here as well! This background will output to #0ff.
  background: smurf.$body;
}

.papa-smurf-hat {
  // This $hat will output to #f00
  background: $hat;
}

Although this system will make naming conflicts a rarity, it does have a caveat; you can no longer declare global variables, functions and mixins and expect them to be available in all partials automatically. I have to create a config partial that forwards everything I want available globally and import it in every single partial. All my scss partials start with the same line;

@use '../config' as *; 

The as * allows a partial to use any variables, functions or mixins without prefacing it with a namespace. Although it takes some getting used to in writing styles, it’s not necessarily a bad thing. Like we are used to in our JS files, you now see all code that is imported at the top of your file and (inter-) dependencies are more clear. It also makes using external packages and libraries more easy; you no longer run the risk of using the same terminology as your imported vendor styles and causing conflicts there.

There is one thing in Dart Sass that is a major drawback for me, however. You may have already noticed it in my code above; I am not @using “01-settings” or “02-tools”, I am @using “config”. Dart Sass does not allow your imported files to start with a number. That means I had to re-think my carefully determined folder order. However, in building a styled-component based React app, I was already having to rethink that structure anyway. I am no longer creating a single main.css file which imports all my components in the order I want them to, I am creating singular components, each with their own styling.

CSS Modules

Harnessing the power of scoped CSS modules in React, I am able to scope styling declarations down even further. I can do the following:

smurf.scss:

@use '../config' as *;

.smurf {
  background: $blue;
}

.hat {
 background: $white;
}

Smurf.js:

import css from '../scss/components/smurf.scss';

export default const Smurf = () => {
  return (
    <>
      <div className={css.hat} />
      <div className={css.smurf} />
    </>
  )
}

papa-smurf.scss:

@use '../config' as *;

.smurf {
  background: $blue;
}

.hat {
  background: $red;
}

PapaSmurf.js:

import css from '../scss/components/papa-smurf.scss';

export default const PapaSmurf = () {
  return (
    <>
      <div className={css.hat} />
      <div className={css.smurf} />
    </>
  )
}

Please ignore the fact that the markup is identical, my example concerns styling, not function. You should note that both Smurf.js as PapaSmurf.js get the exact same className declarations; an object referring to their own SCSS file and its declared classes. What would be outputted to the DOM from these files would be the following:

Smurf.js:

<div class="hat_jn7sd"></div>
<div class="smurf_jkevl"></div>

PapaSmurf.js:

<div class="hat_wirpq"></div>
<div class="smurf_l2nfa"></div>

By integrating scoped modules into my React application, unique CSS classes get rendered for each of them, further disintegrating the need to think of clever naming conventions. I could even import <Smurf /> into my PapaSmurf.js component and their hats would still have a different colour when outputted to the DOM. This is not unique to Dart Sass, in fact it is completely unrelated. You can use scoped CSS modules with Node Sass or even plain CSS as well. Where it becomes interesting, is when you hit the grey areas. More on that later.

Folder Structure

Currently, my setup looks like this:

_src
/components
  /button.js // imports button.scss
  /modal.js // imports modal.scss
  /layout.js // imports global styles
  … etc
/scss
  /components
    button.scss
    modal.scss
    ..etc     
  /config
    _variables.scss
    _mixins.scss
    _functions.scss
    index.scss // index @forwards all partials under the single config namespace
  /global
    _reset.scss
    _typography.scss
    _keyframes.scss
    _layout.scss
    index.scss // index @forwards all partials under the single global namespace

In my styles folder, I now have three major folders; my components folder, which would roughly translate to folders “05-components” and “06-layout” in ye olden days. Config (which translates to folders “01-settings” and “02-tools”) . Finally global, which translates to folders “03-generic” and “07-helpers”. (I no longer use an auto-generated folder, but if I did it would go in my global folder as well).

The config folder shouldn’t generate a single line of CSS but will be imported in nearly every partial in the components and global folders. The global folder should forward all styles that are needed in the global application and its styling will be imported in the wrapper Layout.js file that wraps around my entire React application. Preferably, this style partial should not be very large. Everything in the components folder should be imported in a single React component. One could argue I could also place each application component in its own folder and have each scss partial paired with a JS partial like so:

_src
  /components
    /button
      index.js
      index.scss
    /modal
      index.js
      index.scss

However all the files named “index” become a bit confusing and I find it gets more difficult to keep track of the way my styling should trickle down from smaller to larger entities. SCSS partials and JS partials will follow a very similar pattern in scaling from simple to complicated, but they are not completely one-on-one interchangeable in terms of this pattern. For instance I can have a single Button.js component, but have three wildly different themes to style it. Or I can have some utility JS files that include no styling at all. This way it becomes less clear which partials should go in which folder, so I prefer to split it at the SCSS versus JS level.

Problems and Questions

This system is my current setup, but it is definitely under construction and open to suggestions. For instance, there are some grey areas. My @keyframes partial currently resides in my global styling, because several components make use of them, however they do get outputted to CSS, so I don’t want them in my config. Because of the scoped CSS, however, that means I need to use some funky syntax in order to access them. I can use them like so:

_keyframes.scss:

@keyframes :global(jump) {
  0% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-100%);
  }
  100% {
    transform: translateY(0);
  }
}

_smurf.scss:

.smurf {
  &:global {
    animation: jump .7s;
  }
}

Without specifying the :global parameter, the animation would have been outputted as @keyframes jump_9evk2 for example, and smurf.scss would have no access to the animation at all.

Another tricky thing in my list are fairly generic layout classes. Say I want to do something like a bootstrappy ".col-3" class on a wrapper div which I can use in several places. Do I create an entire React styled wrapper component just to make this styling available, even though it functionally does nothing? Or do I import several style partials in my component; one for generic layout and one for its specific layout? Or do I add generic layout classes to the global styling and do I apply the :global key more often? Perhaps I should create a layout mixin in my config and apply the mixin in different style partials? It’s a puzzle I haven’t fully solved yet.

Finally: a small annoyance I have encountered in using Dart Sass with CSS components is that my compiler starts screaming at me that I’m creating a conflicting SCSS order. It sees I am importing styling in different JS files, then proceeding to use these JS components inside of other JS components (which may or may not be used in the same order and/or have their own style imports) and it firmly believes this will cause trouble. When using scoped CSS components this problem should already be minor, but when the SCSS files are scoped themselves this becomes even less of an issue. There are no interdependencies of outputted CSS using my system, so the order is irrelevant. For now I have simply turned the warnings off.

My pursuit of the perfect styling architecture is far from done. Questions that continue to plague me are how to implement critical above the fold CSS, when- and/or if I should be combining SCSS partials before importing them in my JS components, as well as the questions I mentioned above. Finally; I really do miss my dang numbered folder structure that just gave me my top-down overview.

I’m a lucky girl. But I do have a headache.