Sass just launched a major new feature you might recognize from other languages: a module system. This is a big step forward for @import
. one of the most-used Sass-features. While the current @import
rule allows you to pull in third-party packages, and split your Sass into manageable “partials,” it has a few limitations:
@import
is also a CSS feature, and the differences can be confusing- If you
@import
the same file multiple times, it can slow down compilation, cause override conflicts, and generate duplicate output. - Everything is in the global namespace, including third-party packages – so my
color()
function might override your existingcolor()
function, or vice versa. - When you use a function like
color()
. it’s impossible to know exactly where it was defined. Which@import
does it come from?
Sass package authors (like me) have tried to work around the namespace issues by manually prefixing our variables and functions — but Sass modules are a much more powerful solution. In brief, @import
is being replaced with more explicit @use
and @forward
rules. Over the next few years Sass @import
will be deprecated, and then removed. You can still use CSS imports, but they won’t be compiled by Sass. Don’t worry, there’s a migration tool to help you upgrade!
Import files with @use
@use 'buttons';
The new @use
is similar to @import
. but has some notable differences:
- The file is only imported once, no matter how many times you
@use
it in a project. - Variables, mixins, and functions (what Sass calls “members”) that start with an underscore (
_
) or hyphen (-
) are considered private, and not imported. - Members from the used file (
buttons.scss
in this case) are only made available locally, but not passed along to future imports. - Similarly,
@extends
will only apply up the chain; extending selectors in imported files, but not extending files that import this one. - All imported members are namespaced by default.
When we @use
a file, Sass automatically generates a namespace based on the file name:
@use 'buttons'; // creates a `buttons` namespace
@use 'forms'; // creates a `forms` namespace
We now have access to members from both buttons.scss
and forms.scss
— but that access is not transferred between the imports: forms.scss
still has no access to the variables defined in buttons.scss
. Because the imported features are namespaced, we have to use a new period-divided syntax to access them:
// variables: <namespace>.$variable
$btn-color: buttons.$color;
$form-border: forms.$input-border;
// functions: <namespace>.function()
$btn-background: buttons.background();
$form-border: forms.border();
// mixins: @include <namespace>.mixin()
@include buttons.submit();
@include forms.input();
We can change or remove the default namespace by adding as <name>
to the import:
@use 'buttons' as *; // the star removes any namespace
@use 'forms' as f;
$btn-color: $color; // buttons.$color without a namespace
$form-border: f.$input-border; // forms.$input-border with a custom namespace
Using as *
adds a module to the root namespace, so no prefix is required, but those members are still locally scoped to the current document.
Import built-in Sass modules
Internal Sass features have also moved into the module system, so we have complete control over the global namespace. There are several built-in modules — math
, color
, string
, list
, map
, selector
, and meta
— which have to be imported explicitly in a file before they are used:
@use 'sass:math';
$half: math.percentage(1/2);
Sass modules can also be imported to the global namespace:
@use 'sass:math' as *;
$half: percentage(1/2);
Internal functions that already had prefixed names, like map-get
or str-index
. can be used without duplicating that prefix:
@use 'sass:map';
@use 'sass:string';
$map-get: map.get(('key': 'value'), 'key');
$str-index: string.index('string', 'i');
You can find a full list of built-in modules, functions, and name changes in the Sass module specification.
New and changed core features
As a side benefit, this means that Sass can safely add new internal mixins and functions without causing name conflicts. The most exciting example in this release is a sass:meta
mixin called load-css()
. This works similar to @use
but it only returns generated CSS output, and it can be used dynamically anywhere in our code:
@use 'sass:meta';
$theme-name: 'dark';
[data-theme='#{$theme-name}'] {
@include meta.load-css($theme-name);
}
The first argument is a module URL (like @use
) but it can be dynamically changed by variables, and even include interpolation, like theme-#{$name}
. The second (optional) argument accepts a map of configuration values:
// Configure the $base-color variable in 'theme/dark' before loading
@include meta.load-css(
'theme/dark',
$with: ('base-color': rebeccapurple)
);
The $with
argument accepts configuration keys and values for any variable in the loaded module, if it is both:
- A global variable that doesn’t start with
_
or-
(now used to signify privacy) - Marked as a
!default
value, to be configured
// theme/_dark.scss
$base-color: black !default; // available for configuration
$_private: true !default; // not available because private
$config: false; // not available because not marked as a !default
Note that the 'base-color'
key will set the $base-color
variable.
There are two more sass:meta
functions that are new: module-variables()
and module-functions()
. Each returns a map of member names and values from an already-imported module. These accept a single argument matching the module namespace:
@use 'forms';
$form-vars: module-variables('forms');
// (
// button-color: blue,
// input-border: thin,
// )
$form-functions: module-functions('forms');
// (
// background: get-function('background'),
// border: get-function('border'),
// )
Several other sass:meta
functions — global-variable-exists()
, function-exists()
, mixin-exists()
, and get-function()
— will get additional $module
arguments, allowing us to inspect each namespace explicitly.
Adjusting and scaling colors
The sass:color
module also has some interesting caveats, as we try to move away from some legacy issues. Many of the legacy shortcuts like lighten()
. or adjust-hue()
are deprecated for now in favor of explicit color.adjust()
and color.scale()
functions:
// previously lighten(red, 20%)
$light-red: color.adjust(red, $lightness: 20%);
// previously adjust-hue(red, 180deg)
$complement: color.adjust(red, $hue: 180deg);
Some of those old functions (like adjust-hue
) are redundant and unnecessary. Others — like lighten
. darken
. saturate
. and so on — need to be re-built with better internal logic. The original functions were based on adjust()
. which uses linear math: adding 20%
to the current lightness of red
in our example above. In most cases, we actually want to scale()
the lightness by a percentage, relative to the current value:
// 20% of the distance to white, rather than current-lightness + 20
$light-red: color.scale(red, $lightness: 20%);
Once fully deprecated and removed, these shortcut functions will eventually re-appear in sass:color
with new behavior based on color.scale()
rather than color.adjust()
. This is happening in stages to avoid sudden backwards-breaking changes. In the meantime, I recommend manually checking your code to see where color.scale()
might work better for you.
Configure imported libraries
Third-party or re-usable libraries will often come with default global configuration variables for you to override. We used to do that with variables before an import:
// _buttons.scss
$color: blue !default;
// old.scss
$color: red;
@import 'buttons';
Since used modules no longer have access to local variables, we need a new way to set those defaults. We can do that by adding a configuration map to @use
:
@use 'buttons' with (
$color: red,
$style: 'flat',
);
This is similar to the $with
argument in load-css()
. but rather than using variable-names as keys, we use the variable itself, starting with $
.
I love how explicit this makes configuration, but there’s one rule that has tripped me up several times: a module can only be configured once, the first time it is used. Import order has always been important for Sass, even with @import
. but those issues always failed silently. Now we get an explicit error, which is both good and sometimes surprising. Make sure to @use
and configure libraries first thing in any “entrypoint” file (the central document that imports all partials), so that those configurations compile before other @use
of the libraries.
It’s (currently) impossible to “chain” configurations together while keeping them editable, but you can wrap a configured module along with extensions, and pass that along as a new module.
Pass along files with @forward
We don’t always need to use a file, and access its members. Sometimes we just want to pass it along to future imports. Let’s say we have multiple form-related partials, and we want to import all of them together as one namespace. We can do that with @forward
:
// forms/_index.scss
@forward 'input';
@forward 'textarea';
@forward 'select';
@forward 'buttons';
Members of the forwarded files are not available in the current document and no namespace is created, but those variables, functions, and mixins will be available when another file wants to @use
or @forward
the entire collection. If the forwarded partials contain actual CSS, that will also be passed along without generating output until the package is used. At that point it will all be treated as a single module with a single namespace:
// styles.scss
@use 'forms'; // imports all of the forwarded members in the `forms` namespace
Note: if you ask Sass to import a directory, it will look for a file named index
or _index
)
By default, all public members will forward with a module. But we can be more selective by adding show
or hide
clauses, and naming specific members to include or exclude:
// forward only the 'input' border() mixin, and $border-color variable
@forward 'input' show border, $border-color;
// forward all 'buttons' members *except* the gradient() function
@forward 'buttons' hide gradient;
Note: when functions and mixins share a name, they are shown and hidden together.
In order to clarify source, or avoid naming conflicts between forwarded modules, we can use as
to prefix members of a partial as we forward:
// forms/_index.scss
// @forward "<url>" as <prefix>-*;
// assume both modules include a background() mixin
@forward 'input' as input-*;
@forward 'buttons' as btn-*;
// style.scss
@use 'forms';
@include forms.input-background();
@include forms.btn-background();
And, if we need, we can always @use
and @forward
the same module by adding both rules:
@forward 'forms';
@use 'forms';
That’s particularly useful if you want to wrap a library with configuration or any additional tools, before passing it along to your other files. It can even help simplify import paths:
// _tools.scss
// only use the library once, with configuration
@use 'accoutrement/sass/tools' with (
$font-path: '../fonts/',
);
// forward the configured library with this partial
@forward 'accoutrement/sass/tools';
// add any extensions here...
// _anywhere-else.scss
// import the wrapped-and-extended library, already configured
@use 'tools';
Both @use
and @forward
must be declared at the root of the document (not nested), and at the start of the file. Only @charset
and simple variable definitions can appear before the import commands.
Moving to modules
In order to test the new syntax, I built a new open source Sass library (Cascading Color Systems) and a new website for my band — both still under construction. I wanted to understand modules as both a library and website author. Let’s start with the “end user” experience of writing site styles with the module syntax…
Maintaining and writing styles
Using modules on the website was a pleasure. The new syntax encourages a code architecture that I already use. All my global configuration and tool imports live in a single directory (I call it config
), with an index file that forwards everything I need:
// config/_index.scss
@forward 'tools';
@forward 'fonts';
@forward 'scale';
@forward 'colors';
As I build out other aspects of the site, I can import those tools and configurations wherever I need them:
// layout/_banner.scss
@use '../config';
.page-title {
@include config.font-family('header');
}
This even works with my existing Sass libraries, like Accoutrement and Herman, that still use the old @import
syntax. Since the @import
rule will not be replaced everywhere overnight, Sass has built in a transition period. Modules are available now, but @import
will not be deprecated for another year or two — and only removed from the language a year after that. In the meantime, the two systems will work together in either direction:
- If we
@import
a file that contains the new@use
/@forward
syntax, only the public members are imported, without namespace. - If we
@use
or@forward
a file that contains legacy@import
syntax, we get access to all the nested imports as a single namespace.
That means you can start using the new module syntax right away, without waiting for a new release of your favorite libraries: and I can take some time to update all my libraries!
Migration tool
Upgrading shouldn’t take long if we use the Migration Tool built by Jennifer Thakar. It can be installed with Node, Chocolatey, or Homebrew:
npm install -g sass-migrator
choco install sass-migrator
brew install sass/sass/migrator
This is not a single-use tool for migrating to modules. Now that Sass is back in active development (see below), the migration tool will also get regular updates to help migrate each new feature. It’s a good idea to install this globally, and keep it around for future use.
The migrator can be run from the command line, and will hopefully be added to third-party applications like CodeKit and Scout as well. Point it at a single Sass file, like style.scss
. and tell it what migration(s) to apply. At this point there’s only one migration called module
:
# sass-migrator <migration> <entrypoint.scss...>
sass-migrator module style.scss
By default, the migrator will only update a single file, but in most cases we’ll want to update the main file and all its dependencies: any partials that are imported, forwarded, or used. We can do that by mentioning each file individually, or by adding the --migrate-deps
flag:
sass-migrator --migrate-deps module style.scss
For a test-run, we can add --dry-run --verbose
(or -nv
for short), and see the results without changing any files. There are a number of other options that we can use to customize the migration — even one specifically for helping library authors remove old manual namespaces — but I won’t cover all of them here. The migration tool is fully documented on the Sass website.
Updating published libraries
I ran into a few issues on the library side, specifically trying to make user-configurations available across multiple files, and working around the missing chained-configurations. The ordering errors can be difficult to debug, but the results are worth the effort, and I think we’ll see some additional patches coming soon. I still have to experiment with the migration tool on complex packages, and possibly write a follow-up post for library authors.
The important thing to know right now is that Sass has us covered during the transition period. Not only can imports and modules work together, but we can create “import-only” files to provide a better experience for legacy users still @import
ing our libraries. In most cases, this will be an alternative version of the main package file, and you’ll want them side-by-side: <name>.scss
for module users, and <name>.import.scss
for legacy users. Any time a user calls @import <name>
, it will load the .import
version of the file:
// load _forms.scss
@use 'forms';
// load _forms.input.scss
@import 'forms';
This is particularly useful for adding prefixes for non-module users:
// _forms.import.scss
// Forward the main module, while adding a prefix
@forward "forms" as forms-*;
Upgrading Sass
You may remember that Sass had a feature-freeze a few years back, to get various implementations (LibSass, Node Sass, Dart Sass) all caught up, and eventually retired the original Ruby implementation. That freeze ended last year, with several new features and active discussions and development on GitHub – but not much fanfare. If you missed those releases, you can get caught up on the Sass Blog:
- CSS Imports and CSS Compatibility (Dart Sass v1.11)
- Content Arguments and Color Functions (Dart Sass v1.15)
Dart Sass is now the canonical implementation, and will generally be the first to implement new features. If you want the latest, I recommend making the switch. You can install Dart Sass with Node, Chocolatey, or Homebrew. It also works great with existing gulp-sass build steps.
Much like CSS (since CSS3), there is no longer a single unified version-number for new releases. All Sass implementations are working from the same specification, but each one has a unique release schedule and numbering, reflected with support information in the beautiful new documentation designed by Jina.
Sass Modules are available as of October 1st, 2019 in Dart Sass 1.23.0.
Damn, I’m going to have a lot of projects to migrate! At least they’re giving us a couple of years, one to deprecate and another to remove.
Perfect! I just started a new project and this update came at a very opportune time.
Will implement all these new features on my new project. Thank God i found this article now.
I think when you alias a namespace, you need to omit quotes around the alias.
So this:
@use 'forms' as 'f';
Should be this:
@use 'forms' as f;
Thanks for the article! I’m refactoring my current project now and having fun with the update!
Some very exciting features in store. Looking forward for them to be implemented in node-sass
Am I the only one, who is surprised about the silence in the community for this big new feature? Only 5 comments so far. No discussion on StackOverflow or Reddit (that I’m aware of). No mentioning in the frontend newsletters I subscribed to…
I will need a day of experimenting to get a grasp on the implications. I’ve no idea about the practical implications for big projects. While I get the positive feeling of improving scoping and avoiding bleeding of module stuff into a global scope, I’m still undecided, if this “JS thinking” is really relevant for SCSS/CSS.
On the top of that, are some edge topics like Shadow DOM, STYLE[scoped] and custom properties, which makes the whole scenario even more complex.
With more and more browsers/operating systems getting dark mode, will theming getting more relevant? Theming could be a top application for the new module system – but in 19 years of working with frontend, I got not a single client who wanted me to make multiple themes for his site.
So while I recognize the thinking behind module system, I will need more time and practice to judge the need and the way of implementation into projects.
I do think it’s big news and deserves more attention, but I wouldn’t really call it JS thinking or relate it to Shadow DOM and scoped styles. This is more based on python/nunjucks modules, and only relates to Sass-specific elements like variables, functions, and mixins. It has no direct impact on the output CSS – only on how we combine partials and share libraries in Sass. It is not an attempt to manage DOM scope, themes, or other “modular CSS” concerns. This is very much as Sass language feature, and not a new way of approaching CSS itself. That may be why it’s flown under the radar.
The more I use SCSS the more I like LessJS.
Maybe this is good, but its irritating nonetheless, since implementing a project isn’t as easy as you imply, and we’re back to trying the 100 permutations of uses, forwards, folder structures etc, trying to get our projects to somehow work.
Sigh…
Ok, so I just forget about it and use @import instead – that still works at least.
Maybe it will all become clearer later.
Sheesh, its tough being outside the industry and trying to keep up :(
This is a change for the better.
Interesting development – Like others have said this looks to be like a broadly good idea, but I do have initial concerns about how straight-forwardly the transition will go. Like others have said, I’m also surprised how little I’ve heard about this, as it sounds like a significant development.
I notice that this post doesn’t state the deprecation timeline for @import and the current global functions. From the Sass release post:
It seems that some thought has gone into the deprecation plan, but the timings do seem a little short given the wide variety of projects and libraries relying on Sass.
Seems SASS modules (use of @use, @forward etc.,) is not yet supported in Angular 8.x or higher even though dart-sass is being used.
It is frustrating to note that no where is this issue mentioned in angular site or even in github regarding this. I woke up to this bitter surprise after completing sass folder structure with all the variables and mixins.
I’m new to SCSS
I have this file
// Theme.scss
$palette-primary-dark: #007daa;
and than import into another
// global.scss
@use “./Theme.scss” // omitting .scss like you do results in error
.error {
color: theme.$palette-error; // theme.$palette-error errors with does not exist
}
I can’t reproduce your error with
@use 'Theme'
. That should work fine without any need for./
or.scss
, unless there are other factors in your setup that aren’t clear here. The other issues I see:The variables
$palette-primary-dark
and$palette-error
don’t match, but I assume that’s a copy/paste errorNamespaces are case-sensitive, so you’ll need
Theme.$palette-error
rather thantheme.$palette-error
Having issues with importing compiled css font files using @use. Seems it can’t resolve the file paths, which I think is to do with the @font-face rule being used in these files? Has anyone came across this yet? Or know of a way to import the @font-face blocks in using the @use method?
Font-face shouldn’t cause any issues. If the path is failing to resolve, I would expect there is either an issue with how Sass import paths are configured – or there’s another build tool (webpack?) involved that is changing how paths are resolved.