Recently, a feature has appeared in Sass that is familiar to you in other languages: a modular system . This is a big step forward for
@import
, one of the most commonly used functions in Sass. Despite the fact that the existing
@import
directive allows you to connect third-party packages and separate your styles into supported elements, it still has several limitations :
-
@import
also exists in CSS, and any differences in their behavior can be confusing. - If you do
@import
several times for one file, this can slow down compilation, cause redefinition conflicts, and you get duplicated code in the output. - Everything is in the global scope, including third-party packages - this is how my
color
function can override your existingcolor
function or vice versa. - When you use a function like
color
, it’s impossible to know exactly where it is defined. Which@import
connected it?
The authors of Sass packages (like me) tried to get around namespace problems by manually setting prefixes for variables and functions - but Sass modules are a much more powerful solution. In short,
@import
is
@import
replaced by more explicit
@use
and
@forward
. Over the next few years,
@import
in Sass will be deprecated and then removed. You can still use
CSS Import
's , but they will not be compiled by Sass. But don’t worry, there is a migration tool that will help you upgrade.
Import files using @use
@use 'buttons';
The new
@use
is similar to
@import
, but it has some notable differences:
- The file is imported once, no matter how many times you use
@use
in the project. - Variables, mixins, and functions (which are called “members” in Sass) starting with an underscore (
_
) or hyphen (-
) are considered private and are not imported. - Members from the file connected via
@use
(in our casebuttons.scss
) are accessible only locally and are not transferred to subsequent import. - Similarly,
@extends
will only apply upstream; that is, the extension applies only to styles that are imported, and not to styles that import. - All imported members have their own namespace by default.
When we attach the file via
@use
, Sass automatically generates a namespace based on the file name.
@use 'buttons'; /* `buttons`*/ @use 'forms'; /* `forms`*/
Now we have access to the members of both the
buttons.scss
file and the
forms.scss
file, but this access is not transferred between imports:
forms.scss
still does not have access to the variables defined in
buttons.scss
. Since imported entities have a namespace, we must use the new dot-delimited syntax to access them:
/* : <namespace>.$variable */ $btn-color: buttons.$color; $form-border: forms.$input-border; /* : <namespace>.function() */ $btn-background: buttons.background(); $form-border: forms.border(); /* : @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 *; /* */ @use 'forms' as 'f'; $btn-color: $color; /* buttons.$color */ $form-border: f.$input-border; /* forms.$input-border */
Using
as *
adds the module to the root namespace, so the prefix is not needed, but its members are still locally limited by the current document.
Import Sass Embedded Modules
The internal capabilities in Sass have also been moved to a modular system, so we have full control over the global namespace. There are several built-in modules -
math
,
color
,
string
,
list
,
map
,
selector
and
meta
- which must be imported into the file explicitly before use.
@use 'sass:math'; $half: math.percentage(1/2);
Embedded modules can also be imported into global space:
@use 'sass:math' as *; $half: percentage(1/2);
Built-in functions that already have prefix names, such as
map-get
or
str-index
, can be used without duplicating this prefix:
@use 'sass:map'; @use 'sass:string'; $map-get: map.get(('key': 'value'), 'key'); $str-index: string.index('string', 'i');
You can find a complete list of built-in modules, functions, and name changes in the Sass module specification .
New and changed core features
As an added benefit, this means that Sass can safely add new internal mixins and functions without causing name conflicts. The most amazing example is the
load-css
sass:meta
from the
sass:meta
module. It works by analogy with
@use
, but only returns the generated CSS and works dynamically anywhere in your code:
@use 'sass:meta'; $theme-name: 'dark'; [data-theme='#{$theme-name}'] { @include meta.load-css($theme-name); }
The first argument is the module URL (as in
@use
), but it can be changed dynamically using a variable, even using interpolation, for example
theme-#{$name}
. The second (optional) argument takes a
map
structure with the configuration:
/* $base-color 'theme/dark' */ @include meta.load-css( 'theme/dark', $with: ('base-color': rebeccapurple) );
The
$with
argument allows you to configure any variable in the loaded module using the
map
structure, and this variable must satisfy the conditions:
- It is not a private variable that starts with
_
or-
- Marked with the
!default
Default directive
/* theme/_dark.scss */ $base-color: black !default; /* */ $_private: true !default; /* */ $config: false; /* , !default */
Note that the key
'base-color'
sets the variable
$base-color
.
There are a couple of new functions from the
sass:meta
module
sass:meta
:
module-variables()
and
module-functions()
. Each of them returns a
map
structure from names and values from an already imported module. They take one argument corresponding to 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 functions from
sass:meta
-
global-variable-exists()
,
function-exists()
,
mixin-exists()
, and
get-function()
- will receive additional
$module
arguments that allow us to explicitly check each namespace.
Adjust and scale colors
The
sass:color
module also has some interesting caveats about resolving some of our old problems. Many of the legacy functions like
lighten()
or
adjust-hue()
no longer recommended for use in favor of the explicit functions
color.adjust()
and
color.scale()
:
/* lighten(red, 20%) */ $light-red: color.adjust(red, $lightness: 20%); /* adjust-hue(red, 180deg) */ $complement: color.adjust(red, $hue: 180deg);
Some of these deprecated functions (e.g.
adjust-hue
) are redundant and unnecessary. Others - such as
lighten
,
darken
,
saturate
, etc. - need re-implementation to improve 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 want to change (
scale()
) the color by a certain percentage relative to the current value:
/* 20, 0.2, */ $light-red: color.scale(red, $lightness: 20%);
After being completely deprecated and removed, these functions will eventually reappear in
sass:color
with a new behavior based on
color.scale()
rather than
color.adjust()
. This will happen gradually to avoid sudden backward compatibility issues. In the meantime, I recommend manually checking your code to see where
color.scale()
might be more useful.
Configure Imported Libraries
Third-party or reusable libraries often come with variables with some default values that you can override. We did this with variables before import:
/* _buttons.scss */ $color: blue !default; /* old.scss */ $color: red; @import 'buttons';
Since when using modules there is no longer access to local variables, we need a new way to set values. We can do this by passing the settings via
map
to
@use
:
@use 'buttons' with ( $color: red, $style: 'flat', );
This is similar to the
$with
argument in
load-css()
, but instead of using variable names as keys, we use the variables themselves with the
$
symbol.
I like how explicit the setting has become, but there is one rule that has baffled me several times: a module can only be configured once upon first use . The connection order has always been important for Sass, even with
@import
, but these problems went unnoticed. Now we get a clear error, and this is both good and a bit unexpected. Make sure you connect the libraries via
@use
and configure them in the entry-file (the central document that imports all other files) so that these settings are compiled before other library connections via
@use
.
It is not possible (at the moment) to “link” the configurations together, keeping them editable, but you can wrap the configured module and transfer it as a new module.
Transferring files with @forward
We do not always need to use the file and refer to its members. Sometimes we just want to pass it on to a subsequent import. Suppose we have several files associated with forms, and we want to connect them all together as one namespace. We can do this with
@forward
:
/* forms/_index.scss */ @forward 'input'; @forward 'textarea'; @forward 'select'; @forward 'buttons';
Members of such forwarded files are not available in the current document and no namespace is created, but these variables, functions and mixins will be available when another file connects them via
@use
or
@use
entire collection through
@forward
. If the submitted individual files contain actual CSS, it will also be transmitted without directly generating it until the package itself is used. At this stage, all this will be considered as one module with one namespace:
/* styles.scss */ @use 'forms'; /* `forms` */
Note : If you ask Sass to attach a folder, it will look for the
index
or
_index
file in it.
By default, all public members will be forwarded along with the module. But we can be more selective by using the
show
and
hide
conditions and specifying specific members that we want to add or exclude.
/* `border()` `$border-color` `input` */ @forward 'input' show border, $border-color; /* `buttons` `gradient()` */ @forward 'buttons' hide gradient;
Note : when functions and mixins have a common name, they are added and hidden also together.
To clarify the sources or avoid conflicts of names of forwarded modules, we can add prefixes to the members of the connected file using
as
:
/* forms/_index.scss */ /* @forward "<url>" as <prefix>-*; */ /* , `background()` */ @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 via
@use
and
@forward
the same module through
@forward
, adding both rules:
@forward 'forms'; @use 'forms';
This is especially useful if you want to pre-configure the library or add additional tools before transferring it to other files. This can help simplify connection paths:
/* _tools.scss */ /* */ @use 'accoutrement/sass/tools' with ( $font-path: '../fonts/', ); /* */ @forward 'accoutrement/sass/tools'; /* - ... */ /* _anywhere-else.scss */ /* */ @use 'tools';
Both
@use
and
@forward
must be declared at the root of the document (not nested) and at the beginning of the file. Only
@charset
and simple variable definitions can appear before import directives.
Transition to a modular system
To test the new syntax, I created a new Sass open source library ( Cascading Color Systems ) and a new site for my group - both still under development. I needed to understand the modules from the point of view of the author of the library and from the point of view of the site developer. Let's start with the experience of the "end user" in writing site styles using module syntax ...
Support and writing styles
Using the modules on the site was enjoyable. The new syntax supports the code architecture that I already use. All my imports of global settings and tools are in the same directory (I call it
config
) with an index file that transfers everything I need:
/* config/_index.scss */ @forward 'tools'; @forward 'fonts'; @forward 'scale'; @forward 'colors';
By developing other parts of the site, I can import these tools and configurations wherever I need them:
/* layout/_banner.scss */ @use '../config'; .page-title { @include config.font-family('header'); }
It even works along with my existing libraries, such as Accoutrement and Herman , which still use the old
@import
syntax. Since the
@import
rule
@import
not be replaced everywhere at once, Sass developers have given some time for the transition. Modules are available now, but
@import
will not
@import
another year or two - and will be removed from the language only a year after that. At the same time, the two systems will work together in any way:
- If we execute
@import
for a file that contains the new syntax@use/@forward
, then only public members will be imported without a namespace. - If we execute
@use
or@forward
for a file that contains the old@import
syntax, we get access to all nested imports as a single namespace.
This means that you can immediately start using the new module syntax, without waiting for the release of a new version of your favorite libraries: and I can spend some time updating all my libraries!
Migration tool
The upgrade will not take long if we use the migration tool created by Jennifer Thakar. It can be installed using NPM, Chocolatey or Homebrew:
npm install -g sass-migrator choco install sass-migrator brew install sass/sass/migrator
This is not a one-time tool for migrating to modules. Now that Sass is back in active development (see below), the migration tool will also receive regular updates to help port each new feature. It is a good idea to install this tool globally, and save it for future use.
The migrator can be launched from the command line and, hopefully, will be added to third-party applications such as CodeKit and Scout. Point him to a single Sass file, for example
style.scss
and tell him which migrations to apply. At the moment, there is only one migration called
module
:
# sass-migrator <migration> <entrypoint.scss...> sass-migrator module style.scss
By default, the migrator updates only one file, but in most cases we want to update the main file and all its dependencies: any elements connected via
@import
,
@forward
or
@use
. We can do this by specifying each file individually or simply 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
in abbreviated form) and look at the results without changing the source files. There are a number of other options that we can use to configure migration — there is even one specifically designed to help library authors remove old manually created namespaces — but I won’t describe all of them here. The migration tool is fully documented on the Sass website .
Updating Published Libraries
I encountered several problems on the library side, in particular when I tried to make custom configurations available for several files and find a solution for missing “chain” configurations. Order-related errors can be difficult to debug, but the results are worth the effort, and I think we will see some additional fixes soon. I still need to experiment with the migration tool on complex packages and perhaps write an additional article for library authors.
The important thing to know right now is that Sass provided us with protection during the transition. Not only can the old imports and modules work together, we can create “ import-only ” files to provide more convenient work for users who still connect our libraries via
@import
. In most cases, this will be an alternative version of the main package file, and you want them to be near:
<name>.scss
for module users and
<name>.import.scss
for old users. Each time the user calls
@import <name>
, he loads the
.import
file:
/* `_forms.scss` */ @use 'forms'; /* `_forms.import.scss` */ @import 'forms';
This is especially useful for adding prefixes for developers who do not use modules:
/* _forms.import.scss */ /* */ @forward 'forms' as forms-*;
Sass Update
You may remember that Sass froze the addition of new functions several years ago, so that its various implementations (LibSass, Node Sass, Dart Sass) catch up with the original Ruby implementation, in order to completely abandon it . The freeze ended last year with several new features and active discussions and development on GitHub - but not so solemnly. If you missed these releases, then you can read the Sass blog :
- CSS imports and CSS compatibility (Dart Sass v1.11)
- Content directive parameters and color functions (Dart Sass v1.15)
Currently, Dart Sass is a canonical implementation and is usually the first to introduce new features. I recommend switching to it if you want to receive all the latest. You can install Dart Sass using NPM, Chocolatey or Homebrew. It also works great with gulp-sass .
Like CSS (starting with CSS3), there is no longer a single version number for new releases. All Sass implementations work with the same specification, but each of them has a unique release schedule and numbering, which is reflected in the support information in beautiful new documentation designed by Jina .
Sass modules are available from October 1, 2019 in Dart Sass 1.23.0 .