@use and @forward in Sass

  • SCSS
  • Sass
  • CSS

The new year has me thinking a lot about spring cleaning and general tidying up. One of the small bits of technical debt I've seen across several of my personal projects has been Sass deprecation warnings related to the use of @import, and I decided to finally clean this up this month. Migrating off of @import is a bit of a mental model shift for a Sass project, so in this blog post, I wanted to write about the reasons behind the change, how to use @use and @forward instead (as Sass recommends), what some of the gotchas are for these functions, as well as some of the benefits to this approach.

Example project setup

Here is the folder structure of my little example sass-project where I'll be working through migrating from @import to @use and @forward.

sass-project├── node_modules├── package.json└── styles/    ├── css/    ├── functions/    │   └── _pxToRem_.scss    ├── partials/    │   ├── _spacing.scss    │   └── _typography.scss    └── styles.scss

The contents of my package.json file are as follows:

// package.json{  "name": "sass-project",  "version": "1.0.0",  "scripts": {    "sass": "sass styles/styles.scss:styles/css/styles.css"  },  "dependencies": {    "sass": "^1.83.4"  }}

This minimal setup installs sass when you run npm install or yarn. Running npm run sass or yarn sass compiles the Sass.

The entrypoint is styles/styles.scss:

/* styles/styles.scss */@import "functions/pxToRem";@import "partials/typography";@import "partials/spacing";
.f100 {  @include f100;}
.f200 {  @include f200;}
.f300 {  @include f300;}
.f400 {  @include f400;}
@each $index, $size in $space-map {  .margin-#{$index} {    margin: #{$size};  }}

This entrypoint file imports functions/pxToRem, partials/typography, and partials/spacing:

/* styles/functions/_pxToRem.scss */@use "sass:math";
@function pxToRem($size) {  $remSize: math.div($size, $font-size-base);  @return $remSize * 1rem;}
/* styles/functions/_typography.scss */$font-size--100: 11px;$line-height--100: 1.7;$font-size--200: 13px;$line-height--200: 1.64;$font-size--300: 16px;$line-height--300: 1.5;$font-size--400: 18px;$line-height--400: 1.48;
$font-size-base: $font-size--400;
body {  font-size: $font-size-base;}
@mixin f100 {  font-size: $font-size--100;  line-height: $line-height--100;}
@mixin f200 {  font-size: $font-size--200;  line-height: $line-height--200;}
@mixin f300 {  font-size: $font-size--300;  line-height: $line-height--300;}
@mixin f400 {  font-size: $font-size--400;  line-height: $line-height--400;}
/* styles/functions/_spacing.scss */$size--100: 2px;$size--200: 4px;$size--300: 8px;
$space-map: (  0: 0,  100: pxToRem($size--100),  200: pxToRem($size--200),  300: pxToRem($size--300),) !default;
@function Space($value) {  @return map.get($space-map, $value);}

After those imports, the stylesheet defines some utility classes for a few font sizes and margin sizes based on the tokens defined in _typography.scss and _spacing.scss. It also uses the pxToRem() function to convert pixel values to rems.

The unminified, compiled CSS looks like this:

/* styles/css/styles.css */body {  font-size: 18px;}
.f100 {  font-size: 11px;  line-height: 1.7;}
.f200 {  font-size: 13px;  line-height: 1.64;}
.f300 {  font-size: 16px;  line-height: 1.5;}
.f400 {  font-size: 18px;  line-height: 1.48;}
.margin-0 {  margin: 0;}
.margin-100 {  margin: 0.1111111111rem;}
.margin-200 {  margin: 0.2222222222rem;}
.margin-300 {  margin: 0.4444444444rem;}

However, running the sass script we get some deprecation warnings in the console:

Deprecation Warning [import]: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
More info and automated migrator: https://sass-lang.com/d/import
1 │ @import "functions/pxToRem";  │         ^^^^^^^^^^^^^^^^^^^    styles/styles.scss 1:9  root stylesheet

These are the deprecation warnings I've been wanting to clean up as they occur every time I build in CI. If we follow the link in that warning, we come to this page that details a breaking change related to the use of @import and guides us toward using @use and @forward instead. I'll walk through adjusting our code above to address the deprecation of @import shortly, but it's worth taking a second to dig into the what's going on with the Sass library here.

Why did Sass make this change?

Sass originally built @import to be a way to combine variables, mixins, and functions globally across SCSS file sheets. In our example above, all of the @import statements are at the top of the entrypoint styles/styles.scss file, and are compiled into the output CSS file pretty much in the order they're given in.

The problem is that as a SCSS project grows, the use of global imports can make it hard to know where a variable, mixin, or function is actually defined. I've seen some projects with files such as src/patterns/axioms/Button.scss as well as src/organisms/Button.scss. In a project like that, it can be hard to know exactly where a variable like $Button-height might be defined.

Another problem with @import's global hoisting of variables, mixins, and functions is that variables can get overwritten. The same variable name defined in two files can have its value overwritten if both of those files are imported to the entrypoint.

Switching to @use

To address these issues, Sass introduced @use and @forward a few years ago, and we are currently in a transition period before @import is pretty much entirely removed from Sass (which will happen in an upcoming version).

So how do we start transitioning our code? I'll start in our entrypoint file, and change @import out for @use:

/* styles/styles.scss */@use "functions/pxToRem";@use "partials/typography";@use "partials/spacing";
.f100 {  @include f100;}
.f200 {  @include f200;}
.f300 {  @include f300;}
.f400 {  @include f400;}
@each $index, $size in $space-map {  .margin-#{$index} {    margin: #{$size};  }}

Running the sass script, I get the following error, and our styles.css file compiles incorrectly:

Error: Undefined mixin.6 │   @include f100;  │   ^^^^^^^^^^^^^  styles/styles.scss 6:3  root stylesheet

I get this error because unlike @import, which makes variables, mixins, and functions available globally, @use makes those separate stylesheets available as modules, and their variables, mixins, and functions are available in that module's namespace. So f100 doesn't exist as a mixin, but typography.f100 does. The same goes for the $space-map variable, which is now available from the spacing mixin as spacing.$space-map.

So I will use dot notation to access those mixins and variables from their respective namespaces:

/* styles/styles.scss */@use "functions/pxToRem";@use "partials/typography";@use "partials/spacing";
.f100 {  @include typography.f100;}
.f200 {  @include typography.f200;}
.f300 {  @include typography.f300;}
.f400 {  @include typography.f400;}
@each $index, $size in spacing.$space-map {  .margin-#{$index} {    margin: #{$size};  }}

With the entrypoint file changed above, the compiled CSS is looking closer to what we want:

/* styles/css/styles.css */body {  font-size: 18px;}
.f100 {  font-size: 11px;  line-height: 1.7;}
.f200 {  font-size: 13px;  line-height: 1.64;}
.f300 {  font-size: 16px;  line-height: 1.5;}
.f400 {  font-size: 18px;  line-height: 1.48;}
.margin-0 {  margin: 0;}
.margin-100 {  margin: pxToRem(2px);}
.margin-200 {  margin: pxToRem(4px);}
.margin-300 {  margin: pxToRem(8px);}

The f100, f200, f300, and f400 class rulesets all look good, but we are getting a pxToRem() function call in our compiled CSS for the .margin- classes.

If we hop over to styles/partials/_spacing.scss, we can see where the pxToRem() function calls are coming from. Let's add the namespace to those:

/* styles/partials/_spacing.scss */$size--100: 2px;$size--200: 4px;$size--300: 8px;
$space-map: (  0: 0,  100: pxToRem.pxToRem($size--100),  200: pxToRem.pxToRem($size--200),  300: pxToRem.pxToRem($size--300),) !default;
@function Space($value) {  @return map.get($space-map, $value);}

If we compile our Sass, we get another error:

Error: There is no module with the namespace "pxToRem".7 │   100: pxToRem.pxToRem($size--100),  │        ^^^^^^^^^^^^^^^^^^^^^^^^^^^  styles/partials/_spacing.scss 7:8  @use  styles/styles.scss 3:1             root stylesheet

What gives? We're @useing pxToRem at the top of our entrypoint file, styles/styles.scss, why isn't that namespace available in this subsequent @used file?

Here's the reason from the Sass docs:

Members (variables, functions, and mixins) loaded with @use are only visible in the stylesheet that loads them. Other stylesheets will need to write their own @use rules if they also want to access them.

So we should only @use modules in files that directly need variables, functions, and mixins in that specific file. This means we need to @use the pxToRem module within _spacing.scss:

/* styles/partials/_spacing.scss */@use "../functions/pxToRem";
$size--100: 2px;$size--200: 4px;$size--300: 8px;
$space-map: (  0: 0,  100: pxToRem.pxToRem($size--100),  200: pxToRem.pxToRem($size--200),  300: pxToRem.pxToRem($size--300),) !default;
@function Space($value) {  @return map.get($space-map, $value);}

It also means we need to @use the typography module in _pxToRem.scss to access the $font-size-base variable:

/* styles/functions/_pxToRem.scss */@use "sass:math";@use "../partials/typography";
@function pxToRem($size) {  $remSize: math.div($size, typography.$font-size-base);  @return $remSize * 1rem;}

Finally, it means we can remove the pxToRem module from the top of the styles.scss entrypoint file because that file doesn't directly use it:

/* styles/styles.scss */@use "partials/typography";@use "partials/spacing";
.f100 {  @include typography.f100;}
.f200 {  @include typography.f200;}
.f300 {  @include typography.f300;}
.f400 {  @include typography.f400;}
@each $index, $size in spacing.$space-map {  .margin-#{$index} {    margin: #{$size};  }}

With those adjustments made, we are fully embracing @use, our CSS builds exactly how we want it, and we don't have all of those warnings about the use of @import when we compile our Sass. Our output CSS is back to where we want it:

/* styles/css/styles.css */body {  font-size: 18px;}
.f100 {  font-size: 11px;  line-height: 1.7;}
.f200 {  font-size: 13px;  line-height: 1.64;}
.f300 {  font-size: 16px;  line-height: 1.5;}
.f400 {  font-size: 18px;  line-height: 1.48;}
.margin-0 {  margin: 0;}
.margin-100 {  margin: 0.1111111111rem;}
.margin-200 {  margin: 0.2222222222rem;}
.margin-300 {  margin: 0.4444444444rem;}

Benefits of namespaces

You might be looking at some of these adjustments with a raised eyebrow. That dot notation is kind of annoying, right? Before, I could just @import whatever files I needed and use their variables directly, why do I now need to write out typography.f100, typography.f200, typography.f300, and typography.f400 (in addition to any other variables we need!)?

There is a solution for this that I'll talk about shortly, but personally I kind of like this more verbose approach. I see two benefits here: explicit rules and namespacing.

Explicit rules

The first practical benefit to embracing the module namespaces is that it's a more explicit and understandable mental model for code. When I look at typography.f400, I can very easily mentally trace that f400 is something that exists in the typography module, and because I am bringing that module in at the top of the file with @use "partials/typography";, I know exactly which file to look in to find that f400 mixin.

Say you have multiple files named typography.scss. Perhaps in addition to partials/_typography.scss you also have an axioms/_typography.scss. Perhaps this is the contents of that file:

/* styles/axioms/_typography.scss */$big-font-size: 86px;$font-size-base: $big-font-size;
.big-font-size {  font-size: $big-font-size;}

I've even thrown in a duplicate variable name $font-size-base here to make things extra interesting.

Now, if you try to @use both of those files in the entrypoint...

/* styles/styles.scss */@use "partials/typography";@use "partials/spacing";@use "axioms/typography";
body {  font-size: $font-size-base;}
.f100 {  @include typography.f100;}
.f200 {  @include typography.f200;}
.f300 {  @include typography.f300;}
.f400 {  @include typography.f400;}
@each $index, $size in spacing.$space-map {  .margin-#{$index} {    margin: #{$size};  }}

...You'll get an error at build time telling you that there's a namespace collision:

Error: There's already a module with namespace "typography".1   │ @use "partials/typography";    │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ original @use... │3   │ @use "axioms/typography";    │ ^^^^^^^^^^^^^^^^^^^^^^^^ new @use

The duplicate variable name also sets up conditions in which that variable value could get overwritten. For example, if I swap things back to @import and rearrange the entrypoint file:

/* styles/styles.scss */@import "functions/pxToRem";@import "partials/typography";@import "partials/spacing";
.f100 {  @include f100;}
.f200 {  @include f200;}
.f300 {  @include f300;}
.f400 {  @include f400;}
@each $index, $size in $space-map {  .margin-#{$index} {    margin: #{$size};  }}
@import "axioms/typography";
body {  font-size: $font-size-base;}

If I compile the Sass, we see some very weird behavior:

/* styles/css/styles.css */body {  font-size: 18px;}
body {  font-size: 18px;}
body {  font-size: 18px;}
.f100 {  font-size: 11px;  line-height: 1.7;}
.f200 {  font-size: 13px;  line-height: 1.64;}
.f300 {  font-size: 16px;  line-height: 1.5;}
.f400 {  font-size: 18px;  line-height: 1.48;}
.margin-0 {  margin: 0;}
.margin-100 {  margin: 0.1111111111rem;}
.margin-200 {  margin: 0.2222222222rem;}
.margin-300 {  margin: 0.4444444444rem;}
.big-font-size {  font-size: 86px;}
body {  font-size: 86px;}

We get several repeat rulesets, likely from @importing and @useing files that @import and @use one another:

body {  font-size: 18px;}
body {  font-size: 18px;}
body {  font-size: 18px;}

And, more drastically, our $font-size-base gets overwritten at the end of the file:

body {  font-size: 86px;}

@use helps to prevent this. All @use rules need to be at the top of each file they are used in, so we can't @use a module later on in the file in the way this previous example did with @import. As we saw above, we also can't just @use two separate files named typography, as it will throw an error. If we do need to use two _typography.scss files, we'll need to adjust the namespace of one of them, and we can do so like this:

/* styles/styles.css */@use "partials/typography";@use "partials/spacing";@use "axioms/typography" as axiom-typography;

Now we can use variables from axioms/typography by leveraging the axiom-typography namespace, and the original typograph namespace goes unchanged:

@use "partials/typography";@use "partials/spacing";@use "axioms/typography" as axiom-typography;
.f100 {  @include typography.f100;}
.f200 {  @include typography.f200;}
.f300 {  @include typography.f300;}
.f400 {  @include typography.f400;}
@each $index, $size in spacing.$space-map {  .margin-#{$index} {    margin: #{$size};  }}
body {  font-size: axiom-typography.$font-size-base;}

In this way, we have to explicitly structure our code to overwrite the font-size at the end of that file. It's all stricter and more verbose, but it allows us to write code that signals our intent and helps prevent unwanted problems.

Namespace brevity and safety

The other benefit to fully writing out (for example) typography.f200 or typography.$font-size--200 is that the namespace can become a part of your design system tokens. I've worked in codebases that used @import, and in order to keep design token variable names unique, those variable names were things like $Typography-size--200 or $Typography-size--600-line-height. That's...a lot. typography.$font-size--200 is still a little lengthy, but I personally find it to be nice and idiomatic.

There is also no concern that your typography.$font-size--200 will conflict with a similarly-named variable, either in your own codebase or an external Sass library.

The Sass docs even call this benefit out:

Because @use adds namespaces to member names, it’s safe to choose very simple names like $radius or $width when writing a stylesheet. This is different from the old @import rule, which encouraged that users write long names like $mat-corner-radius to avoid conflicts with other libraries, and it helps keep your stylesheets clear and easy to read!

That said, if you are just absolutely against using the dot notation for modules and their members, then you can also adjust the namespace using the as * wildcard, and it will hoist variables, functions, and mixins to the top-level in a way that is closer to how Sass originally hoisted members with @import:

/* styles/styles.scss */@use "partials/typography" as *;@use "partials/spacing" as *;
.f100 {  @include f100;}
.f200 {  @include f200;}
.f300 {  @include f300;}
.f400 {  @include f400;}
@each $index, $size in $space-map {  .margin-#{$index} {    margin: #{$size};  }}

While this is a little bit cleaner and less verbose, I do think that something is lost in terms of the benefits of explicit namespacing.

Tidying things up with @forward

I also want to touch on @forward, which may also help to make your adjusted Sass code base a little more tidy. As you've seen, we've structured a few partials files within a partials folder:

sass-project├── node_modules├── package.json└── styles/    ├── css/    ├── functions/    │   └── _pxToRem_.scss    ├── partials/    │   ├── _spacing.scss    │   └── _typography.scss    └── styles.scss

Over in our entrypoint, we @use both of those partials explicitly:

/* styles/styles.scss */@use "partials/typography";@use "partials/spacing";

We can do a little work to combine files with @forward. Within the partials folder, I'll create a new file, index.scss:

sass-project├── node_modules├── package.json└── styles/    ├── css/    ├── functions/    │   └── _pxToRem_.scss    ├── partials/    │   ├── _spacing.scss    │   ├── _typography.scss    │   └── index.scss    └── styles.scss

That index.scss file will simply @forward along our two partials files:

/* styles/partials/index.scss */@forward "./typography";@forward "./spacing";

With that set up, we can now adjust the entrypoint to use that index of partials:

/* styles/styles.scss */@use "partials";
.f100 {  @include partials.f100;}
.f200 {  @include partials.f200;}
.f300 {  @include partials.f300;}
.f400 {  @include partials.f400;}
@each $index, $size in partials.$space-map {  .margin-#{$index} {    margin: #{$size};  }}

The "partials" namespace correlates to everything @forwarded by /partials/index.scss, effectively standing in for what was previously both the typography and spacing namespaces. We can even do the top-level namespace hoisting:

@use "partials" as *;
.f100 {  @include f100;}
.f200 {  @include f200;}
.f300 {  @include f300;}
.f400 {  @include f400;}
@each $index, $size in $space-map {  .margin-#{$index} {    margin: #{$size};  }}

Wrapping up

There's a lot more to explore with this new way of writing Sass, but more advanced uses are beyond the scope of a short blog post. You should hopefully walk away better understanding:

  • That @import is now almost entirely discouraged in Sass because its global hoisting can cause mental overhead, build performance, and namespace collision issues
  • How to use @use along with namespaces to achieve largely the same work you did previously with @import
  • That @use namespacing is more verbose, but that more explicit code and namespaces can help to convey intent in code and provide variable/mixin/function name safety
  • That you can adjust namespaces using as or use @forward to further help with tidying up your Sass workspace