@use and @forward in Sass
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 @use
ing pxToRem
at the top of our entrypoint file, styles/styles.scss
, why isn't that namespace available in this subsequent @use
d 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 @import
ing and @use
ing 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 @forward
ed 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