Sass で変数や mixin を別ファイルに分けるとき、長いあいだ使われてきたのが @import でした。ところがこの @import は非推奨(deprecated)になり、これから書くコードでは @use と @forward を使うことが推奨されています。
この記事では、@import の何が問題だったのか、@use の基本と名前空間の考え方、複数のパーシャルをまとめる @forward、sass:math などの組み込みモジュール、そして既存のコードを移行するときにつまずきやすいポイントまでを、SCSS 記法のコード付きで解説します。
目次
@import が非推奨になった理由
@import は「書いた場所に、そのファイルの中身をそのまま展開する」という素朴な仕組みでした。動きが単純で分かりやすい反面、ファイル数が増えてくると次のような問題が表面化します。
すべてがグローバルに置かれる
@import で読み込んだファイルの変数・mixin・関数は、すべて同じひとつの空間に置かれます。つまり、あるファイルで $color という変数を定義し、別のファイルでも $color を定義していると、あとから読み込まれたほうが前の値を静かに上書きしてしまいます。エラーにはならないので、色が違うことに気づいてから原因を探すことになります。
これは自分が書いたファイル同士だけの話ではありません。外部のライブラリを読み込んだ場合、そのライブラリが内部で使っている変数や関数まで全部グローバルに並びます。名前がぶつからないようにするために、ライブラリ側は $mylib-primary-color のように長い接頭辞を付けて自衛するしかありませんでした。
同じファイルが何度も読み込まれる
@import は書いた回数だけ展開します。_buttons.scss と _cards.scss がどちらも @import "variables"; と書いていて、style.scss がその2つを読み込むと、_variables.scss の中身は2回展開されます。
変数の定義だけなら実害は少ないのですが、パーシャルの中にスタイルルールが書いてあると、まったく同じ CSS が重複して出力されます。ファイルサイズが膨らむうえ、同じ詳細度のルールが2回出てくるので、意図しない上書きの原因にもなります。
どこで定義された変数か分からない
すべてがグローバルなので、コードの中に $base-color と書かれていても、それがどのファイルで定義されたものなのかは読んだだけでは分かりません。プロジェクトが大きくなるほど、定義元を探してファイルを横断的に検索する作業が増えていきます。
これらの問題を解決するために導入されたのが、モジュールシステムである @use と @forward です。@import は Dart Sass 1.80.0 で正式に非推奨となり、使うとコンパイル時に警告が出るようになりました。将来の Dart Sass 3.0.0 で言語から削除される予定です。
なお、@use に対応しているのは Dart Sass(npm の sass パッケージ)です。古いプロジェクトでよく使われていた node-sass(LibSass)はモジュールシステムに対応しないまま開発が終了しているので、移行する前にまず Dart Sass へ切り替える必要があります。
@use の基本とパーシャル
まず、変数をまとめたファイルを用意します。ファイル名の先頭にアンダースコアを付けたものをパーシャルと呼び、これ単体では CSS にコンパイルされません。_variables.scss から variables.css が出力されることはない、ということです。読み込む側のファイルに取り込まれて初めて意味を持ちます。
$primary: #0d6efd;
$text-color: #333;
$space: 16px;
これを @use で読み込みます。パスを書くときは、先頭のアンダースコアと拡張子を省略するのが慣例です(付けて書いても動きます)。
@use "variables";
.button {
// 「名前空間.変数名」で参照する
background-color: variables.$primary;
color: #fff;
padding: variables.$space;
}
.button {
background-color: #0d6efd;
color: #fff;
padding: 16px;
}
@import との最大の違いは、variables.$primary のように前に名前空間を付けて参照するところです。書く量は増えますが、そのぶん「この変数は _variables.scss で定義されたものだ」とコードを見ただけで分かります。定義元を探し回る必要がなくなるのは、実際に書いてみると思った以上に快適です。
もうひとつ重要なのが、@use で読み込んだメンバーはそのファイルの中でだけ有効という点です。_buttons.scss が @use "variables" と書いていても、_buttons.scss を読み込んだ側が自動的に variables を使えるようにはなりません。変数を使いたいファイルは、それぞれ自分で @use を書きます。グローバル汚染が起きないのはこのルールのおかげです。
名前空間を変える・省略する
名前空間のデフォルトはファイル名(パスの最後の部分から、先頭のアンダースコアと拡張子を除いたもの)です。@use "abstracts/variables" と書けば名前空間は variables になります。ディレクトリ名は含まれません。
as で短い名前に付け替える
ファイル名が長いと、参照のたびに長い接頭辞を書くことになります。as を使うと好きな名前空間に変えられます。
@use "abstracts/variables" as v;
.button {
background-color: v.$primary;
padding: v.$space;
}
別々のディレクトリに同じファイル名のパーシャルがある場合も、as で名前空間を分ければ衝突しません。同じ名前空間を2つの @use で使おうとするとエラーになるので、そのときも as の出番です。
as * で名前空間を省略する
as * を付けると、読み込んだメンバーを名前空間なしで使えます。@import に近い書き味になるので、移行の第一歩としてはこの形が楽です。
@use "variables" as *;
.button {
// 名前空間なしで書ける
background-color: $primary;
padding: $space;
}
ただし as * を多用すると、せっかくの「定義元が分かる」という利点が失われます。名前がぶつかったときもエラーで気づくしかなくなるので、使うなら変数をまとめたファイル1つだけにする、といった線引きをしておくと安全です。
アンダースコアで始まるメンバーは公開されない
名前空間と一緒に覚えておきたいのが、プライベートメンバーです。変数名や mixin 名を - または _ で始めると、そのファイルの中だけで使える非公開の存在になり、@use した側からは参照できません。ファイル内部の計算用にだけ使う値を、外に見せずに済ませられます。
// 外に公開される
$space: 16px;
// 先頭がハイフンなので、このファイルの中だけで使える
$-base-unit: 4px;
同じファイルは1回しか読み込まれない
@use は、同じファイルを何箇所から読み込んでも実際に評価されるのは1回だけです。_buttons.scss と _cards.scss が両方とも @use "variables" と書いていても、_variables.scss が2回処理されることはありません。
この性質は、スタイルルールを含むパーシャルで効いてきます。@import では重複して出力されていた CSS が、@use では1回だけ出力されます。「重複しないように読み込む順番を管理する」という作業から解放されるわけです。
もうひとつのルールが書く位置です。@use はファイルの先頭に書かなければいけません。正確には、@charset・@forward・(設定用の)変数宣言以外のルールより前に置く必要があります。スタイルルールを書いたあとに @use を続けると、コンパイルエラーになります。
.header {
color: #333;
}
// NG: スタイルルールより後ろには書けない
@use "variables";
@import と @use の違い
ここまでの内容を対比して整理すると、次のようになります。
| 項目 | @import | @use |
|---|---|---|
| 状態 | 非推奨。将来削除される予定 | 推奨される書き方 |
| 変数・mixin の扱い | すべてグローバルに置かれる | 名前空間ごとに分かれる |
| 参照の書き方 | $primary | variables.$primary(as * なら名前空間なし) |
| 同じファイルを複数回読み込んだとき | 書いた回数だけ展開される | 1回しか読み込まれない |
| 読み込んだものの有効範囲 | 以降のファイルにも引き継がれる | 書いたファイルの中だけ |
| 書ける位置 | どこでも書ける | ファイルの先頭 |
| 変数の上書き | 読み込む前に同名の変数を定義しておく | with で設定する |
| 再公開 | そのまま引き継がれる | @forward を使う |
@forward で複数のパーシャルをまとめる
@use したものが次のファイルに引き継がれないということは、変数・mixin・関数のファイルが5つあれば、使う側で5行の @use を書くことになります。これを1行にまとめてくれるのが @forward です。
@forward は「読み込んだファイルのメンバーを、自分のメンバーとしてそのまま外へ公開する」ルールです。@use と違い、書いたファイル自身がそのメンバーを使えるようになるわけではない点に注意してください(自分でも使いたい場合は @use も一緒に書きます)。
ここで便利なのがインデックスファイルです。ディレクトリの中に _index.scss を置いておくと、@use "abstracts" のようにディレクトリ名を指定するだけでそのファイルが読み込まれます。
@forward "variables";
@forward "mixins";
@forward "functions";
// _index.scss が読み込まれ、3つのファイルのメンバーがまとめて使える
@use "../abstracts" as a;
.button {
background-color: a.$primary;
padding: a.$space;
@include a.flex-center;
}
名前空間は abstracts(ディレクトリ名)になりますが、上のように as a で短くしておくと書きやすくなります。
as prefix-* で接頭辞を付ける
@forward "..." as prefix-* と書くと、公開されるメンバー名すべてに接頭辞が付きます。複数のファイルをまとめたときに、どのファイル由来のメンバーか分かるようにしたいときに使います。
// _variables.scss の $primary が $color-primary として公開される
@forward "variables" as color-*;
@forward "mixins";
@use "../abstracts" as a;
.button {
background-color: a.$color-primary; // 接頭辞が付いた名前で参照する
}
show と hide で公開する範囲を絞る
ファイルの中身を全部公開したくないときは、show で公開するメンバーを列挙するか、hide で隠したいメンバーを指定します。変数は $ 付き、mixin と関数は名前だけを書きます。
// $primary と $space、mixin の flex-center だけを公開する
@forward "variables" show $primary, $space, flex-center;
// 逆に、指定したものだけを隠す
@forward "mixins" hide legacy-clearfix;
ライブラリとして配布するファイルなら公開範囲を絞る価値がありますが、自分のプロジェクト内で完結するなら、まずは @forward を並べるだけで十分です。
sass:math などの組み込みモジュール
モジュールシステムの導入で、Sass 本体が用意している関数も @use で読み込む形に変わりました。sass: で始まる特別な URL を指定します。
| モジュール | 内容 |
|---|---|
sass:math | 割り算・丸め・平方根などの数値演算 |
sass:color | 色を明るくする、透明度を変えるなどの色操作 |
sass:string | 文字列の連結・切り出し・置換 |
sass:list | リストの取得・追加・結合 |
sass:map | マップからの値の取得・追加・マージ |
sass:selector | セレクタの解析・結合 |
sass:meta | Sass の内部情報を扱う(変数の存在確認など) |
よく使うのは sass:math の math.div() です。かつて割り算に使われていたスラッシュ($a / $b)は、CSS 側でスラッシュが値の区切り文字として使われるようになったため非推奨になりました。割り算には math.div() を使います。
@use "sass:math";
@use "variables" as v;
.card {
// $space の半分を余白にする
padding: math.div(v.$space, 2);
// 切り上げ・切り捨てなども用意されている
width: math.percentage(math.div(2, 3));
}
.card {
padding: 8px;
width: 66.6666666667%;
}
組み込みモジュールは Sass 本体に含まれているので、ファイルを用意する必要はありません。名前空間はデフォルトで math・color のようにコロンの後ろの部分になります。
@use に変えたら変数が見つからないとき
@import から書き換えた直後に多いのが、これまで動いていた変数や mixin が「ない」と言われるパターンです。原因はだいたい次の4つに分類できます。
名前空間を付け忘れている
@import の感覚のまま $primary と書いていると、Undefined variable. というエラーになります。@use "variables" で読み込んだ変数は variables.$primary と書かないと見つかりません。mixin も同じで、@include variables.flex-center; のように名前空間を付けます。
逆に、名前空間は付けたけれど名前を間違えている場合は There is no module with the namespace "..." という別のエラーになります。@use "abstracts/variables" の名前空間は variables であって abstracts ではない、というのがよくある勘違いです。エラーメッセージのどちらが出ているかで、原因を切り分けられます。
@use の位置がファイルの先頭ではない
@import は途中に書けたので、「このあたりで使うからここで読み込む」という書き方をしていたファイルがあるかもしれません。@use にはこれが通用せず、スタイルルールより後ろに書くとエラーになります。コメントは何行書いても問題ないので、先頭のコメントブロックのあとにまとめて並べてください。
例外は @charset、@forward、そして次に説明する with で使う設定用の変数宣言です。これらは @use より前に書けます。
別のファイルで @use したものは引き継がれない
style.scss の先頭で @use "variables" と書き、そのあと @use "components/button" と書いているのに、_button.scss の中で変数が見つからない、というケースです。
@use の有効範囲は書いたファイルの中だけなので、_button.scss でも @use を書く必要があります。これは不便に見えますが、「どのファイルが何に依存しているか」がファイル単位で完結するということでもあります。依存が増えてきたら、前述の _index.scss でまとめて1行にしてしまうのがおすすめです。
変数を上書きするには with を使う
@import の時代は、読み込む前に同じ名前の変数を定義しておけば値を差し替えられました。@use ではこの方法が使えず、代わりに with で設定を渡します。
ここで重要なのが、with で上書きできるのは !default を付けて定義された変数だけという点です。!default は「その変数がまだ定義されていない(または null)場合にだけ、この値を使う」という意味のフラグです。上書きされる可能性がある変数には、必ず付けておきます。
// !default を付けた変数だけが with で上書きできる
$primary: #0d6efd !default;
$radius: 4px !default;
// これは上書きできない(付け忘れるとエラーになる)
$font-family: sans-serif;
@use "theme" with (
$primary: #e91e63,
$radius: 0
);
.button {
background-color: theme.$primary; // #e91e63 が使われる
border-radius: theme.$radius; // 0 が使われる
}
!default が付いていない変数を with で指定すると、「設定できない変数だ」という内容のエラーになります。また、ひとつのモジュールを設定付きで読み込めるのは1回だけで、そのモジュールが別の場所ですでに読み込まれたあとに with 付きで @use することもできません。設定は、エントリーポイントとなる style.scss の先頭で1回だけ行う、と決めておくと混乱しません。
既存のコードを移行する手順
ファイル数が少なければ手作業で置き換えられますが、規模が大きいプロジェクトには Sass 公式の移行ツール sass-migrator が用意されています。@import を @use と @forward に書き換え、変数の参照に名前空間を付けるところまで自動でやってくれます。
# インストール
npm install -g sass-migrator
# エントリーポイントと、そこから読み込まれるファイルをまとめて移行する
sass-migrator module --migrate-deps style.scss
--migrate-deps を付けると、指定したファイルが依存しているパーシャルも一緒に書き換えます。ファイルを直接上書きするので、必ず Git でコミットしてから実行してください。どう変わるかを先に見たいときは --dry-run --verbose を付けると、変更内容だけが表示されます。
ツールを使った場合も、出力された名前空間が長すぎたり、_index.scss にまとめたほうが見通しが良かったりすることはあります。コンパイルが通ることを確認したうえで、as での短縮や @forward による整理を手で入れていくと、無理なく移行できます。
まとめ
@import は、すべてをグローバルに展開してしまう・同じファイルを何度も読み込んでしまう・定義元が追えない、という理由で非推奨になりました。代わりの @use は、読み込んだメンバーを名前空間の下に置き、variables.$primary のように出どころが分かる形で参照します。同じファイルは何回書いても1回しか読み込まれず、書ける位置はファイルの先頭に限られます。
読み込んだものが他のファイルに引き継がれないぶん、複数のパーシャルをまとめたいときは @forward と _index.scss を使います。変数を差し替えたいときは !default を付けたうえで with で設定します。既存プロジェクトの移行には sass-migrator が使えるので、Git でコミットしてから一度走らせてみるとよいでしょう。