Sassを@importから@useに置き換えるための手引き

Sassの@importルールは、廃止されることが予定されています。
@useや@forwardといったルールに置き換わるのですが、これらは全く新しいシステムです。
これらの知見が溜まってきたのでご紹介します。

@import と 新しいモジュールシステム

Sassの初期の方から実装されていた@importは、テキスト形式でSassファイルを読み込むための機能です。便利な機能ですが、CSSの@importと重複しているため、SassなのかCSSなのか一見してわかりにくい側面がありました。

新しいモジュールシステムは、この問題を解消し、Sassをより柔軟に使用できるようにします。
@importではグローバルに指定されていた変数などは、ファイルごとにカプセル化され、ファイルの内容に基づいて解決できるようになりました。
現在、Dart Sassしか新しいモジュールシステムは使えませんが、LibSassにも実装される予定です。
もし試してみたい方は node-sass ではなく sass を使ってみましょう。

新しい機能が使えることは喜ばしいことですが、残念なお知らせもあります。
@importの完全削除が予定されています。
CSSの@importはそのまま使えますが、Sassの機能である@importは削除されるということです。

具体的な予定は、Sassの公式ブログ内の「Future Plans」に、今後のサポートの流れが記載されています。
Dart SassとLibSassの両方がモジュールシステムのサポートを開始してから1年後、またはDart Sassがモジュールシステムのサポートを開始してから2年後、どちらか早い時期から@importを非推奨にします。
さらに非推奨が有効になった1年後に@importおよびほとんどのグローバル関数のサポートを完全に削除されます。

原文の方に書かれていますが、遅くとも2022年10月1日には廃止されるようです。

2022年9月29日追記
GitHubのsass より、LibSassそのものが非推奨となり、推奨されるsassのコンパイラはDartSassのみとなってしまいました。
@importは非推奨のままですが、80%のユーザーがDartSassを使用するようになるまで、最低1年以上(2023年以降)を待って@importを削除する予定に変更したようです。

@use とは何か?

@useルールは、メンバー(変数やMixins、関数)をカプセル化し、読み込んだスタイルシートメタ言語のみに適用させます。
また@useで読み込んだファイル名が名前空間になり、その名前空間から各メンバーを参照できます。

@use "variables";
@use "mixins";

.element {
  background-color: variables.$body-bg;
  
  @include mixins.layout;
}

名前空間はas節によって変更可能です。

@use "variables" as *;
@use "mixins" as mx;

.element {
  background-color: $body-bg;
  
  @include mx.layout;
}

カプセル化の影響によって、変数はグローバルに適用されなくなりました。変数を外部から変更する場合は、with節を使います。
!defaultフラグが使用されている変数のみを変更できます。

// _demo.scss
$margin-top: 1rem;
$margin-bottom: 1rem !default; // ここだけ変更可能

p {
  margin-top: $margin-top;
  margin-bottom: $margin-bottom;
}
@use "demo" with (
  $margin-bottom: 2rem
);

@forward とは何か?

@useで読み込むと変数やMixinsといったメンバーは、読み込んだファイル以外は、参照できません。
孫の変数を出力しようとしている下記のコードはエラーとなります。

// _child.scss
$a: 1 !default; 
$b: 2 !default; 
// _parent.scss
@use 'child';
@use 'hoge';
@use 'fuga';
@use 'parent';

.foo-parent {
  content: parent.child.$a; // エラーになる
  content: parent.child.$b; // エラーになる
}

上記の「_parent.scss」のようにハブとなるファイルに使用するのが、@forwardルールです。
@forwardは、Sassのライブラリ作成用のルールと思っていただければいいでしょう。

// _child.scss
$a: 1 !default; 
$b: 2 !default; 
// _parent.scss
@forward 'child';
@forward 'hoge';
@forward 'fuga';
@use 'parent';

.foo-parent {
  content: parent.$a; // 1
  content: parent.$b; // 2
}

エラーがでることなく、変数を呼び出せました。 @forwardは、複数のライブラリを読み込むだけなく、中の変数も操作できます。 「_child.scss」を変更したい場合は、with節を使って「_parent.scss」の@forwardから変数を変更できますが、最終的な読み込み先である「@use 'parent'」からもwith節で変数を変更できます。

// _parent.scss
@forward 'child' with ( $a: 10 !default );
@forward 'hoge';
@forward 'fuga';
@use 'parent' with ( $b: 40 );

.foo-parent {
  content: parent.$a; // 10
  content: parent.$b; // 40
}

さらにエクスポート可能な関数やMixinsを制御したり、名前空間にプレフィックスを設定できます。 @forwardの詳細は、公式サイトをご覧ください

ビルトインモジュール とは?

今まで使用されていたグローバル関数は廃止対象となります。
そしてグローバル関数はモジュール化され、「ビルトインモジュール」と呼ばれるものに生まれ変わります。

モジュール化により、通常のCSSとSassの関数が競合せずに扱えるようになりました。
そのためSassの新しい関数の追加が楽になり、今後新しいモジュールも追加される予定だそうです。

ビルトインモジュールの使い方は、@useを使用してモジュールを読み込み、関数を使用します。

@use "sass:map";
@use "sass:meta";

@function mapDeepGet($map, $keys...) {
  @if (meta.type-of($map) == map) {
    @debug 'Error';
    @return;
  }

  $pmap: $map;
  @each $key in $keys {
    @if not map.has-key($pmap, $key) {
      @return null;
    }
    $pmap: map.get($pmap, $key);
  }

  @return $pmap;
}

このモジュール化によって、一部の関数のメソッド名が変更されています。
また削除や新しい機能をもった関数も登場する予定とされています。

詳しくは、公式サイトをご確認ください

新しいモジュールシステム は @import の代替ではない

@useなどのカプセル化によって、@importの時のような使い方はできません。
どのくらい使用感が変わるのか。
できることと、できないことを細く見てみましょう。

変数の扱い方

@importで変数を先に読むと、その子孫のファイルにも影響してファイルの子孫に依存せずに変数を扱えました。
しかし@useは、メンバー(変数やMixins、関数)をカプセル化し、読み込んだスタイルシートメタ言語のみ適用させます。
そのため指定したい変数が、親や兄弟、孫関係のファイルであったとしても、エラーがでます。

// _variables.scss

$space: 8px;
// _padding.scss

.padding {
  padding: $space;
}
// エラーになる
@use 'variables' as *;
@use 'padding';

またメタ言語内で変数を記述し、グローバルに指定していた要領で、@useで読み込んだ先の子のファイルに変数を当ててようとしてもエラーとなります。

// エラーになる
$space: 8px;

@use 'padding';

子のファイルの変数を変更したい場合は、with節と!defautフラグを使いましょう。

変数が適用されない例として取りあげましたが、他のメンバーの関数やMixinsも同様です。
別ファイル内のメンバーを使いたい場合は、@useを使ってファイルごとに読み込んで使用してください。

同じファイル名の場合は、名前空間を変える

@useの読み込む時に、ディレクトリ名は違うけどファイル名が同じ場合、名前空間を変えていないとエラーが出ます。

@use 'variables/main';
@use 'functions/main';

as節を使って名前空間を変えましょう。

@use 'variables/main' as variables;
@use 'functions/main' as functions;

もし同じ名前空間上で複数のファイルを利用したい場合は、@forwardを記述するファイルを用意します。
そして、そのファイルを経由すると名前空間が同じになります。

// _forward_file.scss

@forward 'variables/main';
@forward 'functions/main';

関数などで先頭にアンダースコアを使ったら呼び出せない

独自の関数「_hoge()」を作成し、別のファイルで呼びだそうとすると、エラーになります。

Error: Private members can't be accessed from outside their modules.

メンバー(変数、関数、Mixins)の先頭にハイフンやアンダースコアを使用すると、それはプライベートメンバーとなります。
プライベートメンバーとなったメンバーは、そのファイル内でしか利用できません。
別ファイルで呼び出してもエラーとなります。

// _round

$_radius: 3px !default; // このファイルでしか使えない変数

@mixin _round { // このファイルでしか使えないMixin
  border-radius: $_radius;
}

では、プライベートメンバーである「$_radius」変数は上書きできるでしょうか?
with節を使えば可能です。プライベートメンバーは外部出力はできませんが、入力による変更は可能です。

@use 'round' with (
  $_radius: 20px // これはOK
);

.foo {
  border-radius: round.$_radius; // これはNG
}

@use や @forward は、先に置かなければならない

@importから@useに移行する際など、@useの位置を下の方に置きたい時もあります。
しかし@importや宣言ブロックよりも下に@useを記述すると、エラーになります。

// エラーになる
@import 'variables';
@import 'bootstrap';

@import 'reset-css';

.style {
  color: #000;
}

@use 'component/demo';

@useを利用する場合は、@importや宣言ブロックよりも上に配置しなければなりません。

// OK
@use 'component/demo';

@import 'variables';
@import 'bootstrap';

@import 'reset-css';

.style {
  color: #000;
}

ただしWeb Fontの読み込みで「@import url('https://xxxx')」を使用する時など、@importを一番下に記述してもCSSの出力は最上部に記述されるようです。

下記が記述例です。
とても気持ち悪いですが、そういうものみたいです。

// Dart Sass
@use 'component/demo1';
@use 'component/demo2';
@use 'component/demo3';

@import url('https://xxxx');


// CSSの出力
@import url('https://xxxx');

/* component/demo1以下のスタイル */
.component-class {}

extendがカジュアルに行えない

私はSassのextendを、あまり使っていませんでしたが、新しいモジュールシステムになってから、より使いづらくなった印象です。

@use 'components/utility';
@use 'components/layout';

「components/utility」の中にある「%extend」を「components/layout」の中で@extendしたくてもできません。
@useはカプセル化します。そのため親や兄弟関係にあるメンバーはもちろん、CSSセレクタも共有されません。

別ファイル同士でextendを行う場合は、extend用のファイルを用意します。
extend用のファイルにプレースホルダーセレクタを記述し、@useで読み込んでextendを行うようにしましょう。

// _extend.scss
%display-block {
  display: block;
}
@use 'extend';

.block {
  @extend %display-block;
}

一般的によく見るSassの構造が使えない

現在多くの方が実装されている変数やMixinsなどが綺麗に分かれたSassの構成は、新しいモジュールシステムでは使えません。

下記のコードはBootstrap v4の抜粋です。
@importは指定されたメンバーが、グローバルとして全てのファイル内で共有されます。

// scss-docs-start import-stack
// Configuration
@import "functions";
@import "variables";
@import "mixins";
@import "utilities";

// Layout & components
@import "root";
@import "reboot";
@import "type";
@import "images";

グローバルであるため変数の読み込み順さえ、気をつければ、簡単に変数を上書きすることも可能です。 下記はBootstrapで、変数の上書きを行う1つの例です。

// 先頭に上書き用の変数を配置
@import "variables-new";

// Configuration
@import "bootsrap/functions";
@import "bootsrap/variables";
@import "bootsrap/mixins";
@import "bootsrap/utilities";

// Layout & components
@import "bootsrap/root";
@import "bootsrap/reboot";
// @import "bootsrap/type";
// @import "bootsrap/images";

Bootstrapの変数には全て!defaultフラグを指定されているので、上書き用の変数ファイルを先頭に記述するだけで、全てのインポート先に変数の値が適用されます。簡単に色やフォントサイズなどを変更できます。

しかし、新しいモジュールシステムは、メンバーがファイルごとにカプセル化されるので、最初に変数などを読み込んでも全体に適用されません。
そしてBootstrap v4のファイル構成のまま、単純に@importから@useに置き換えただけでは、大量のエラーを吐き出します。

この構成を維持したまま@useに対応できるように修正しても、@importの時のような使いやすいファイル構成にはなりません。
@useでは、かなり使い辛い構成となっています。

新しいモジュールシステムへ変更してみましょう。
まずは、変数や関数といったメンバーのファイルの読み込みをルートのファイルから外し、@importを@useに変更します。

// Configuration
@use "bootsrap/utilities";

// Layout & components
@use "bootsrap/root";
@use "bootsrap/reboot";
// @use "bootsrap/type";
// @use "bootsrap/images";

変数の置き換えは2つの方法があります。 各ファイルの変数ファイルのパスを変更するか、または変数ファイルのパスはそのままでwith節を使って、変数を上書きするかの2通りです。

変数ファイルのパスの変更を見てみましょう。
下記は、Bootstrap v4の「mixins/_button.scss」のファイルです。

@use "sass:color";
@use "../functions";
@use "../../../variables-new" as variables; // ここのバスを変更
@use "../vendor/rfs";
@use "border-radius";
@use "box-shadow";
@use "gradients";
@use "hover";

// Button variants
//
// Easily pump out default styles, as well as :hover, :focus, :active,
// and disabled options for all buttons

@mixin button-variant($background, $border, 

次に同じファイルを使って、with節を使った方法も見てみます。

@use "sass:color";
@use "../functions";
@use "../variables" with (
  $btn-box-shadow: 0 0 0 #000,
  $btn-active-box-shadow: 0 0 3px #000,
  $btn-focus-width: 2px
);
@use "../vendor/rfs";
@use "border-radius";
@use "box-shadow";
@use "gradients";
@use "hover";

// Button variants
//
// Easily pump out default styles, as well as :hover, :focus, :active,
// and disabled options for all buttons

@mixin button-variant($background, $border, 

変数のパスを変える、もしくはwith節を使って変数を上書きする方法のどちらでもいいのですが、この変更を変数を使用している「全てのファイル」でパス、もしくは変数を再設定しなければいけません。

とても面倒くさいですね。 コアとなるファイルも含めて全て変更しなければならないので、Bootstrapのようなライブラリをアップデートする時は、バックアップを取りつつ、再び変数を設定し直す必要が出てきます。
現実的ではない変更方法です。

新しいモジュールシステム に対応するには?

Sassは入れ子しか使っていない、という方は、全く影響はありません。
問題は、変数やMixins、関数といったメンバーを使ってテクニカルに構築している開発者や@importで構築されているフレームワークやライブラリを多用している開発者です。

Sassの@importは、廃止される予定のルールです。 将来的なことを考えれば、今すぐに@importによるグルーバルなファイル構成に依存しない方がいいでしょう。

私は、新しいモジュールシステムで構築してみて、下記のファイル構成がいいのではないかと思いました。
変数を独立したファイルとせずに、関連するメンバーごとに1つのファイルにまとめる方法です。

scss
  style.scss
  base/
  tools/
    _global.scss
    helper/
      _functions.scss
      _meadiaqueries.scss
    components/
      _button.scss // components/buttonで使いたい
      _layout.scss // components/layoutで使いたい
  components/
    _about.scss
    _button.scss
    _layout.scss

上記は、サンプルとして用意したディレクトリ構造です。
サンプルファイルはGitHubにあげています。

style.scss は、CSSとして生成されるファイルです。
また「base」ディレクトリは、リセットCSSなどが格納されているファイルです。

「scss/tools」ディレクトリの「_global.scss」と「helperディレクトリ」は、@forward用のファイルと汎用的にライブラリを格納するためのフォルダです。

「scss/tools/components」ディレクトリは、各コンポーネントで必要になったメンバーがある場合は、必要なコンポーネント名でファイルを用意し、そこに必要な変数や関数、Mixinsを1つのファイルで管理します。

そして「scss/components」ディレクトリは、今までのようにコンポーネントのスタイルを管理するのに利用します。

それぞれを細かく見てみましょう。

style.scss の中身

style.scssは、ファイルを読み込むだけのファイルです。
例は以下の通りです。toolsディレクトリは、ここでは読み込みません。

// style.scss

@use 'base/normalize';

@use 'components/about';
@use 'components/button';
@use 'components/layout';

tools/_global.scss の中身

style.scssでは、toolsディレクトリを読み込んでいませんが、先に中身を見ておきましょう。
サイトの全体で使用するような汎用的なライブラリ(関数やMixins集)は「tools/_global.scss」の中で「@forward」を使用して設定しておきます。

// tools/_global.scss

// @forward
@forward 'helper/functions';
@forward 'helper/meadiaqueries';

components/_button.scss の中身

style.scssで読み込んだコンポーネントのスタイルを見てみましょう。

// components/_button.scss

@use '../tools/global';
@use '../tools/components/button';

// Font Size
$font-size-base: 16px !default;
$font-size-large: 18px !default;
$font-size-medium: 14px !default;
$font-size-small: 12px !default;

.c-button {
  padding: button.$spacing-pc;
  font-size: $font-size-base;

  @include global.mq(lg) {
    padding: button.$spacing-mobile;
  }
}

.c-button {
  @include button.generator;
}

先ほど@forwardで読み込んだ汎用的に使用するライブラリの「tools/tools」と、ボタンコンポーネントのメンバーファイルである「tools/button」を@useで読み込んで使用します。

@useで読み込んだファイルは、何もしなければファイル名が名前空間として与えられます。 汎用的に使用するライブラリも「global」という名前空間が割り当てられ「global.mq()」といった感じに呼び出せます。

変数やMixinsのメンバーがまとまったファイル

「tools/components/button」の抜粋を見てみましょう。 変数とMixinsが同じファイル内で同居しています。 また変数を変更できるように「!default」フラグもつけています。

// tools/components/_button.scss

@use "sass:map";

// Variables
$spacing-pc: 8px !default;
$spacing-mobile: 4px !default;

$pattern: () !default;
$_pattern: map.merge(
  (
    primary: (
      default: (
        background: #aaa,
        color: #000
      ),
      hover: (
        background: #fff,
        color: #aaa
      )
    ),
    success: (
      default: (
        background: #aaa,
        color: #000
      ),
      hover: (
        background: #fff,
        color: #bbb
      )
    )
  ),
  $pattern
);

// Mixins
@mixin generator() {
 // buttonのパターンを自動生成
}

もう一つMediaqueriesの出し分けを行うファイルの抜粋も見ておきましょう。

// tools/helper/_mediaqueries.scss

@use "sass:map";
@use "sass:meta";
@use 'functions';

// Variables
$breakpoint: () !default;
$_breakpoint: map.merge(
  (
    sm: 576px,
    md: 768px,
    lg: 992px,
    xl: 1200px
  ),
  $breakpoint
);

// Mixins
@mixin mq($size, $range: 'max') {
  // Mediaqueriesの処理
}

@mixin mq-only($screen-min, $screen-max) {
  // Mediaqueriesの処理
}

変数を独立したファイルとせずに、1つのファイルにまとまっているのが確認できます。

この方法の利点は、SassをBourbonのようなライブラリを独自で作成し、使い回したい時に楽になります。 このようなライブラリは、読み込んでいる関数やMixinsのファイルそのものの内容は、変数であっても変えたくありません。

先ほどBootstrapの@useに置き換える例を出しましたが、@useで変数を別ファイルにしてしまうと、読み込んでいる処理系の関数やMixinsのファイルを修正しなければならない状況に陥る場合もあります。
特に、フレームワークやライブラリの「パッケージ」として配信している場合はそれが顕著です。

フレームワークは、外側から容易に変更を加えられる仕組みが必要です。
その1つの解が、関連する変数やMixinsを1つのファイルにまとめることです。

コンポーネントのボタンの例を見てみましょう。

// components/_button.scss

@use '../tools/global';
@use '../tools/components/button';

// Font Size
$font-size-base: 16px !default;
$font-size-large: 18px !default;
$font-size-medium: 14px !default;
$font-size-small: 12px !default;

.c-button {
  padding: button.$spacing-pc;
  font-size: $font-size-base;

  @include global.mq(lg) {
    padding: button.$spacing-mobile;
  }
}

.c-button {
  @include button.generator;
}

「@use 'tools/components/button'」は変数とMixinsのまとまったファイルです。
この変数を変更して、ボタンのサイズやボタンのバリエーションを増やしたい場合は、tools内のファイルを触らずに「with」節を用いるだけで変更できます。

// components/_button.scss

@use 'tools/tools' as *;
@use 'tools/components/button' with (
  $spacing-pc: 12px,
  $spacing-mobile: 8px,
  $pattern: (
    primary: (
      default: (
        background: blue,
        color: #fff
      )
    ),
    original: (
      default: (
        background: #aaa,
        color: #000
      )
    )
  )
);

// Font Size
$font-size-base: 16px !default;
$font-size-large: 18px !default;
$font-size-medium: 14px !default;
$font-size-small: 12px !default;

.c-button {
  padding: button.$spacing-pc;
  font-size: $font-size-base;

  @include global.mq(lg) {
    padding: button.$spacing-mobile;
  }
}

.c-button {
  @include button.generator;
}

コンポーネントごとのMixinsファイルであれば、この方法で変数の値を変更できます。
また汎用ライブラリの変数を変更したい場合も「tools/_global.scss」というライブラリのハブに対して、with節で変数を変更して対応できます。

「tools」ディレクトリをフレームワークのパッケージとして独立して扱う場合も、この方法であればコアを触らずに欲しいMixinsや関数を取り出して使うことも容易です。

独立した変数ファイルを用意した運用

変数や関数といったメンバーをまとめる方法を紹介しましたが、変数の独立したファイルを用意して、運用する方法もできなくはありません。

下記のソースコードのように、変数ファイルの読み込んだ上でMixinsなどと同居するファイル内で、変数を再定義してあげます。

// components/_button.scss

@use 'variables'; // 独立した変数ファイルを用意

@use 'tools/tools' as *;
@use 'tools/components/button' with (
  $spacing-pc: variables.$spacing-pc,
  $spacing-mobile: variables.$spacing-mobile,
  $pattern: variables.$button-pattern
);

// Font Size
$font-size-base: variables.$font-size-base !default;
$font-size-large: variables.$font-size-large !default;
$font-size-medium: variables.$font-size-medium !default;
$font-size-small: variables.$font-size-small !default;

.c-button {
  padding: button.$spacing-pc;
  font-size: $font-size-base;

  @include global.mq(lg) {
    padding: button.$spacing-mobile;
  }
}

.c-button {
  @include button.generator;
}

このように変数を再定義してあげると、このファイルを@useで読み込んだとしてもwith節で変数を上書きできます。
この方法は、すべての値を変数で統一する、というよりかは、with節で上書きするためのプレースホルダーという認識で行う方がいいでしょう。

なお、グローバル変数的なものをもっと簡単に設定したいのであれば、Sassの機能ではなく、CSSカスタムプロパティを使った方が早いでしょう。

:root {
  --demo: #000;
  --demo-text: #000;
}

.foo {
  background-color: var(--demo);
  color: var(--demo-text);
}

注意として、CSSカスタムプロパティはIEで使えません。
PostCSSのカスタムプロパティのプラグインによってIEも対応できますが、CSSで実装されているものと使用感が全く違うので、あまりおすすめしません。

@import で作成されている現在のフレームワークについて

Sassの入れ子ぐらいしか使ってないよ、という方は、@importが廃止になったとしても影響は皆無です。

しかし現在のBootstrapやFoudationなどで作成されているフレームワークや、複雑なMixinsや関数を使って作成されている開発者は、@importが廃止になった時に影響を大きく受けます。
@importから@useにSassのファイルを書き換える移行ツールは、Sassの公式サイトで公開されていますが、Sassの構成によっては、移行が難しい場合もあります。

結局は、開発者が手で直すところも出てくるでしょう。
実際にBootstrap v4を移行ツールに通してみましたが、このツールを実行しただけでは「functions」と「variables」の間でループしてしまって、Sassのコンパイルがうまくいきませんでした。

Sassをこれからも使っていくのであれば、@importが廃止されるまでに、@useなどの特性は理解しておくべきでしょう。

また今回はフレームワーク的な目線で変数とMixinsなどを1つのファイルにまとめて運用する方法を紹介しました。Sassのファイルをフレームワークとして使い回さずに運用するのであれば、変数を独立したファイルとして、各ファイルに読み込む方法もアリかと思います。

ただ私もSassを使っていくうちに、今回記事にしたSassの構成は変わるかもしれませんし、頭のいい人が考えたベストプラクティス的なものも出てくるかもしれません。

しかし、Sassをどのように運用したいかで、サイトごとのモジュールシステムの扱い方は大きく変わってくることでしょう。