{"id":296858,"date":"2019-10-07T07:24:56","date_gmt":"2019-10-07T14:24:56","guid":{"rendered":"https:\/\/css-tricks.com\/?p=296858"},"modified":"2019-10-14T11:06:17","modified_gmt":"2019-10-14T18:06:17","slug":"introducing-sass-modules","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/introducing-sass-modules\/","title":{"rendered":"Introducing Sass Modules"},"content":{"rendered":"
Sass just launched a major new feature you might recognize from other languages: a module system<\/strong>. This is a big step forward for Sass package authors (like me) have tried to work around the namespace issues by manually prefixing our variables and functions \u2014 but Sass modules are a much more powerful solution. In brief, <\/p>\n The new When we We now have access to members from both We can change or remove the default namespace by adding Using 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 \u2014 Sass modules can also be imported to the global namespace:<\/p>\n Internal functions that already had prefixed names, like You can find a full list of built-in modules, functions, and name changes in the Sass module specification<\/a>.<\/p>\n 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 The first argument is a module URL (like The Note that the There are two more Several other The Some of those old functions (like Once fully deprecated and removed, these shortcut functions will eventually re-appear in 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:<\/p>\n 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 This is similar to the I love how explicit this makes configuration, but there\u2019s one rule that has tripped me up several times: a module can only be configured once, the first time it is used<\/strong>. Import order has always been important for Sass, even with It\u2019s (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.<\/p>\n We don\u2019t always need to use a file, and access its members. Sometimes we just want to pass it along to future imports. Let\u2019s say we have multiple form-related partials, and we want to import all of them together as one namespace. We can do that with 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 Note:<\/strong> if you ask Sass to import a directory, it will look for a file named By default, all public members will forward with a module. But we can be more selective by adding Note:<\/strong> when functions and mixins share a name, they are shown and hidden together.<\/p>\n In order to clarify source, or avoid naming conflicts between forwarded modules, we can use And, if we need, we can always That\u2019s 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:<\/p>\n Both In order to test the new syntax, I built a new open source Sass library (Cascading Color Systems<\/a>) and a new website for my band<\/a> \u2014 both still under construction. I wanted to understand modules as both a library and website author. Let\u2019s start with the “end user” experience of writing site styles with the module syntax\u2026<\/p>\n 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 As I build out other aspects of the site, I can import those tools and configurations wherever I need them:<\/p>\n This even works with my existing Sass libraries, like Accoutrement<\/a> and Herman<\/a>, that still use the old 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!<\/p>\n Upgrading shouldn\u2019t take long if we use the Migration Tool<\/a> built by Jennifer Thakar. It can be installed with Node, Chocolatey, or Homebrew:<\/p>\n 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\u2019s a good idea to install this globally, and keep it around for future use.<\/p>\n The migrator can be run from the command line, and will hopefully be added to third-party applications<\/a> like CodeKit and Scout as well. Point it at a single Sass file, like By default, the migrator will only update a single file, but in most cases we\u2019ll want to update the main file and all its dependencies<\/em>: any partials that are imported, forwarded, or used. We can do that by mentioning each file individually, or by adding the For a test-run, we can add 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\u2019ll 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. <\/p>\n 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<\/a>” files to provide a better experience for legacy users still This is particularly useful for adding prefixes for non-module users:<\/p>\n 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<\/a>. That freeze ended last year, with several new features and active discussions and development<\/a> on GitHub \u2013 but not much fanfare. If you missed those releases, you can get caught up on the Sass Blog<\/a>:<\/p>\n 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<\/a> with Node, Chocolatey, or Homebrew. It also works great with existing gulp-sass<\/a> build steps.<\/p>\n 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<\/a> designed by Jina<\/a>.<\/p>\n@import<\/code>. one of the most-used Sass-features. While the current
@import<\/code> rule allows you to pull in third-party packages, and split your Sass into manageable “partials,” it has a few limitations:<\/p>\n
\n
@import<\/code> is also a CSS feature, and the differences can be confusing<\/li>\n
@import<\/code> the same file multiple times, it can slow down compilation, cause override conflicts, and generate duplicate output.<\/li>\n
color()<\/code> function might override your existing
color()<\/code> function, or vice versa.<\/li>\n
color()<\/code>. it\u2019s impossible to know exactly where it was defined. Which
@import<\/code> does it come from?<\/li>\n<\/ul>\n
@import<\/code> is being replaced with more explicit
@use<\/code> and
@forward<\/code> rules. Over the next few years Sass
@import<\/code> will be deprecated, and then removed. You can still use CSS imports<\/a>, but they won\u2019t be compiled by Sass. Don\u2019t worry, there\u2019s a migration tool<\/a> to help you upgrade!<\/p>\n
Import files with @use<\/h3>\n
@use 'buttons';<\/code><\/pre>\n
@use<\/code> is similar to
@import<\/code>. but has some notable differences:<\/p>\n
\n
@use<\/code> it in a project.<\/li>\n
_<\/code>) or hyphen (
-<\/code>) are considered private, and not imported.<\/li>\n
buttons.scss<\/code> in this case) are only made available locally, but not passed along to future imports.<\/li>\n
@extends<\/code> will only apply up the chain;<\/em> extending selectors in imported files, but not extending files that import this one.<\/li>\n
@use<\/code> a file, Sass automatically generates a namespace based on the file name:<\/p>\n
@use 'buttons'; \/\/ creates a `buttons` namespace\r\n@use 'forms'; \/\/ creates a `forms` namespace<\/code><\/pre>\n
buttons.scss<\/code> and
forms.scss<\/code> \u2014 but that access is not transferred between the imports:
forms.scss<\/code> still has no access to the variables defined in
buttons.scss<\/code>. Because the imported features are namespaced, we have to use a new period-divided syntax to access them:<\/p>\n
\/\/ variables: <namespace>.$variable\r\n$btn-color: buttons.$color;\r\n$form-border: forms.$input-border;\r\n\r\n\/\/ functions: <namespace>.function()\r\n$btn-background: buttons.background();\r\n$form-border: forms.border();\r\n\r\n\/\/ mixins: @include <namespace>.mixin()\r\n@include buttons.submit();\r\n@include forms.input();<\/code><\/pre>\n
as <name><\/code> to the import:<\/p>\n
@use 'buttons' as *; \/\/ the star removes any namespace\r\n@use 'forms' as f;\r\n\r\n$btn-color: $color; \/\/ buttons.$color without a namespace\r\n$form-border: f.$input-border; \/\/ forms.$input-border with a custom namespace<\/code><\/pre>\n
as *<\/code> adds a module to the root namespace, so no prefix is required, but those members are still locally scoped to the current document.<\/p>\n
Import built-in Sass modules<\/h3>\n
math<\/code>,
color<\/code>,
string<\/code>,
list<\/code>,
map<\/code>,
selector<\/code>, and
meta<\/code> \u2014 which have to be imported explicitly in a file before they are used:<\/p>\n
@use 'sass:math';\r\n$half: math.percentage(1\/2);<\/code><\/pre>\n
@use 'sass:math' as *;\r\n$half: percentage(1\/2);<\/code><\/pre>\n
map-get<\/code> or
str-index<\/code>. can be used without duplicating that prefix:<\/p>\n
@use 'sass:map';\r\n@use 'sass:string';\r\n$map-get: map.get(('key': 'value'), 'key');\r\n$str-index: string.index('string', 'i');<\/code><\/pre>\n
New and changed core features<\/h3>\n
sass:meta<\/code> mixin called
load-css()<\/code>. This works similar to
@use<\/code> but it only returns generated CSS output, and it can be used dynamically anywhere in our code:<\/p>\n
@use 'sass:meta';\r\n$theme-name: 'dark';\r\n\r\n[data-theme='#{$theme-name}'] {\r\n @include meta.load-css($theme-name);\r\n}<\/code><\/pre>\n
@use<\/code>) but it can be dynamically changed by variables, and even include interpolation, like
theme-#{$name}<\/code>. The second (optional) argument accepts a map of configuration values:<\/p>\n
\/\/ Configure the $base-color variable in 'theme\/dark' before loading\r\n@include meta.load-css(\r\n 'theme\/dark', \r\n $with: ('base-color': rebeccapurple)\r\n);<\/code><\/pre>\n
$with<\/code> argument accepts configuration keys and values for any variable in the loaded module, if it is both: <\/p>\n
\n
_<\/code> or
-<\/code> (now used to signify privacy)<\/li>\n
!default<\/code> value, to be configured<\/li>\n<\/ul>\n
\/\/ theme\/_dark.scss\r\n$base-color: black !default; \/\/ available for configuration\r\n$_private: true !default; \/\/ not available because private\r\n$config: false; \/\/ not available because not marked as a !default<\/code><\/pre>\n
'base-color'<\/code> key will set the
$base-color<\/code> variable.<\/p>\n
sass:meta<\/code> functions that are new:
module-variables()<\/code> and
module-functions()<\/code>. Each returns a map of member names and values from an already-imported module. These accept a single argument matching the module namespace:<\/p>\n
@use 'forms';\r\n\r\n$form-vars: module-variables('forms');\r\n\/\/ (\r\n\/\/ button-color: blue,\r\n\/\/ input-border: thin,\r\n\/\/ )\r\n\r\n$form-functions: module-functions('forms');\r\n\/\/ (\r\n\/\/ background: get-function('background'),\r\n\/\/ border: get-function('border'),\r\n\/\/ )<\/code><\/pre>\n
sass:meta<\/code> functions \u2014
global-variable-exists()<\/code>,
function-exists()<\/code>,
mixin-exists()<\/code>, and
get-function()<\/code> \u2014 will get additional
$module<\/code> arguments, allowing us to inspect each namespace explicitly. <\/p>\n
Adjusting and scaling colors<\/h4>\n
sass:color<\/code> module also has some interesting caveats, as we try to move away from some legacy issues. Many of the legacy shortcuts like
lighten()<\/code>. or
adjust-hue()<\/code> are deprecated for now in favor of explicit
color.adjust()<\/code> and
color.scale()<\/code> functions:<\/p>\n
\/\/ previously lighten(red, 20%)\r\n$light-red: color.adjust(red, $lightness: 20%);\r\n\r\n\/\/ previously adjust-hue(red, 180deg)\r\n$complement: color.adjust(red, $hue: 180deg);<\/code><\/pre>\n
adjust-hue<\/code>) are redundant and unnecessary. Others \u2014 like
lighten<\/code>.
darken<\/code>.
saturate<\/code>. and so on \u2014 need to be re-built with better internal logic. The original functions were based on
adjust()<\/code>. which uses linear math: adding
20%<\/code> to the current lightness of
red<\/code> in our example above. In most cases, we actually want to
scale()<\/code> the lightness by a percentage, relative to the current value:<\/p>\n
\/\/ 20% of the distance to white, rather than current-lightness + 20\r\n$light-red: color.scale(red, $lightness: 20%);<\/code><\/pre>\n
sass:color<\/code> with new behavior based on
color.scale()<\/code> rather than
color.adjust()<\/code>. 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()<\/code> might work better for you.<\/p>\n
Configure imported libraries<\/h3>\n
\/\/ _buttons.scss\r\n$color: blue !default;\r\n\r\n\/\/ old.scss\r\n$color: red;\r\n@import 'buttons';<\/code><\/pre>\n
@use<\/code>:<\/p>\n
@use 'buttons' with (\r\n $color: red,\r\n $style: 'flat',\r\n);<\/code><\/pre>\n
$with<\/code> argument in
load-css()<\/code>. but rather than using variable-names as keys, we use the variable itself, starting with
$<\/code>. <\/p>\n
@import<\/code>. but those issues always failed silently. Now we get an explicit error, which is both good and sometimes surprising. Make sure to
@use<\/code> and configure libraries first thing in any “entrypoint” file (the central document that imports all partials), so that those configurations compile before other
@use<\/code> of the libraries.<\/p>\n
Pass along files with @forward<\/h3>\n
@forward<\/code>:<\/p>\n
\/\/ forms\/_index.scss\r\n@forward 'input';\r\n@forward 'textarea';\r\n@forward 'select';\r\n@forward 'buttons';<\/code><\/pre>\n
@use<\/code> or
@forward<\/code> 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:<\/p>\n
\/\/ styles.scss\r\n@use 'forms'; \/\/ imports all of the forwarded members in the `forms` namespace<\/code><\/pre>\n
index<\/code> or
_index<\/code>)<\/p>\n
show<\/code> or
hide<\/code> clauses, and naming specific members to include or exclude:<\/p>\n
\/\/ forward only the 'input' border() mixin, and $border-color variable\r\n@forward 'input' show border, $border-color;\r\n\r\n\/\/ forward all 'buttons' members *except* the gradient() function\r\n@forward 'buttons' hide gradient;<\/code><\/pre>\n
as<\/code> to prefix members of a partial as we forward:<\/p>\n
\/\/ forms\/_index.scss\r\n\/\/ @forward \"<url>\" as <prefix>-*;\r\n\/\/ assume both modules include a background() mixin\r\n@forward 'input' as input-*;\r\n@forward 'buttons' as btn-*;\r\n\r\n\/\/ style.scss\r\n@use 'forms';\r\n@include forms.input-background();\r\n@include forms.btn-background();<\/code><\/pre>\n
@use<\/code> and
@forward<\/code> the same module by adding both rules:<\/p>\n
@forward 'forms';\r\n@use 'forms';<\/code><\/pre>\n
\/\/ _tools.scss\r\n\/\/ only use the library once, with configuration\r\n@use 'accoutrement\/sass\/tools' with (\r\n $font-path: '..\/fonts\/',\r\n);\r\n\/\/ forward the configured library with this partial\r\n@forward 'accoutrement\/sass\/tools';\r\n\r\n\/\/ add any extensions here...\r\n\r\n\r\n\/\/ _anywhere-else.scss\r\n\/\/ import the wrapped-and-extended library, already configured\r\n@use 'tools';<\/code><\/pre>\n
@use<\/code> and
@forward<\/code> must be declared at the root of the document (not nested), and at the start of the file. Only
@charset<\/code> and simple variable definitions can appear before the import commands.<\/p>\n
Moving to modules<\/h3>\n
Maintaining and writing styles<\/h4>\n
config<\/code>), with an index file that forwards everything I need:<\/p>\n
\/\/ config\/_index.scss\r\n@forward 'tools';\r\n@forward 'fonts';\r\n@forward 'scale';\r\n@forward 'colors';<\/code><\/pre>\n
\/\/ layout\/_banner.scss\r\n@use '..\/config';\r\n\r\n.page-title {\r\n @include config.font-family('header');\r\n}<\/code><\/pre>\n
@import<\/code> syntax. Since the
@import<\/code> rule will not be replaced everywhere overnight, Sass has built in a transition period. Modules are available now, but
@import<\/code> will not be deprecated for another year or two \u2014 and only removed from the language a year after that. In the meantime, the two systems will work together in either direction:<\/p>\n
\n
@import<\/code> a file that contains the new
@use<\/code>\/
@forward<\/code> syntax, only the public members are imported, without namespace.<\/li>\n
@use<\/code> or
@forward<\/code> a file that contains legacy
@import<\/code> syntax, we get access to all the nested imports as a single namespace.<\/li>\n<\/ul>\n
Migration tool<\/h4>\n
npm install -g sass-migrator\r\nchoco install sass-migrator\r\nbrew install sass\/sass\/migrator<\/code><\/pre>\n
style.scss<\/code>. and tell it what migration(s) to apply. At this point there\u2019s only one migration called
module<\/code>:<\/p>\n
# sass-migrator <migration> <entrypoint.scss...>\r\nsass-migrator module style.scss<\/code><\/pre>\n
--migrate-deps<\/code> flag:<\/p>\n
sass-migrator --migrate-deps module style.scss<\/code><\/pre>\n
--dry-run --verbose<\/code> (or
-nv<\/code> 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 \u2014 even one specifically for helping library authors remove old manual namespaces \u2014 but I won\u2019t cover all of them here. The migration tool is fully documented<\/a> on the Sass website<\/a>.<\/p>\n
Updating published libraries<\/h4>\n
@import<\/code>ing our libraries. In most cases, this will be an alternative version of the main package file, and you\u2019ll want them side-by-side:
<name>.scss<\/code> for module users, and
<name>.import.scss<\/code> for legacy users. Any time a user calls
@import <name><\/code>, it will load the
.import<\/code> version of the file:<\/p>\n
\/\/ load _forms.scss\r\n@use 'forms';\r\n\r\n\/\/ load _forms.input.scss\r\n@import 'forms';<\/code><\/pre>\n
\/\/ _forms.import.scss\r\n\/\/ Forward the main module, while adding a prefix\r\n@forward \"forms\" as forms-*;<\/code><\/pre>\n
Upgrading Sass<\/h3>\n
\n