Dart Sass (@use, @forward 使用)で Bootstrap 4 の変数やマップを上書きする

Dart Sass (@use, @forward 使用)で Bootstrap 4 の変数・マップを上書きしたくなったので実験してみました。

今まで (node-sass / LibSass)

前提

   /
  └ src/
    ├ html/
    │    └ index.html
    │
    └ scss/
       ├ assets/
       │  └ bootstrap/
       │    │    └ bootstrap/
       │    │         └ 略
       │    └ bootstrap.scss
       │
       ├ foundation/
       │    ├ _index.scss
       │    ├ _mixin.scss
       │    └ _variables.scss
       │
       ├ layout/
       │    ├ _l-footer.scss
       │    ├ _l-header.scss
       │    └ _l-main.scss
       │
       ├ object/
       │    ├ component/
       │    │    └ 略
       │    ├ project/
       │    │    └ 略
       │    └ utility/
       │         └ 略
       │
       └ index.scss

今回の検証で使用するプロジェクトのディレクトリ構造は、このような状態とします。

src/html/index.html

<div class="mt-4">
    <a href="#" class="btn btn-primary m-3">プライマリーボタン</a><!-- プライマリーカラーのボタン -->
    <a href="#" class="btn btn-main m-3">メインボタン</a><!-- デフォルトにはないボタン -->
</div>

例として、こんな HTML があったとして。

src/scss/foundation/_scss_variable.scss

$theme-colors: (
    /* 上書き */
    primary: $own-color,
    /* デフォルトにないカラーの追加 */
    main: $own-main-color,
);

マップの定義を上書きするコードを書きます。

src/scss/foundation/_index.scss

@import "variables"; //変数(Bootstrap の変数上書きのコードあり)
@import "mixin";
@import "../assets/bootstrap/bootstrap"; //bootstrap

同じ foundation の中に読み込み用の Scss を用意します。

注意する点としては、「 Bootstrap の Scss を読み込むより前に、 Bootstrap の変数(マップ)を上書きするための自前の定義が書かれた Scss を読み込む」ということ。

src/scss/index.scss

@import "./foundation/index"; //読み込み

最後に、実際に index.css にコンパイルされる src/scss/index.scsssrc/scss/foundation/_index.scss を読み込みます。

今までは、これで変数の上書きができました。

Dart Sass での検証1 (失敗 / 単純に @import@use, @forward に書き換え)

さて、ここで単純に今まで @import で記述していたのを @use, @forward に書き換えてみます。

src/scss/foundation/_scss_variable.scss

$theme-colors: (
    /* 上書き */
    primary: $own-color,
    /* デフォルトにないカラーの追加 */
    main: $own-main-color,
);

上書きしたい変数(マップ)の定義はそのまま。

src/scss/foundation/_index.scss

@forward "scss_variables"; //変数(Bootstrap の変数上書きのコードあり)
@forward "mixin";
@forward "../assets/bootstrap/bootstrap"; //bootstrap

今度は @import ではなく、 @forward に書き換えます。

src/scss/layout/_l-header.scss

@use "../foundation" as f;

.l-header {
    .navbar-brand {
        &,
        &:link,
        &:visited {
            color: f.$own-main-color;
        }
        &:hover,
        &:active,
        &:focus {
            color: f.$own-main-color_l;
        }
    }
}

// 略

実際に src/scss/foundation/_index.scss を読み込んで使用する Scss で @use による読み込みを行います。

src/scss/index.scss

@use "layout/l-header"; //_l-header.scss の中で @use を使って src/scss/foundation/_index.scss を読み込み、使用
@use "layout/l-main";
@use "layout/l-footer";

こんな形に書き換えます。

流れとしては、「src/scss/index.scss -(@use)-> src/scss/layout/_l-header.scss -(@forward)-> src/scss/foundation/_index.scss」という関係。

この状態で Dart Sass によるコンパイルを実行すると、$theme-colors の変数名が重複しているのでエラーになってしまいます。

Error: src/scss/foundation/_index.scss
Error: Two forwarded modules both define a variable named $theme-colors.
    ╷
3   │ @forward "variables";
    │ ━━━━━━━━━━━━━━━━━━━━ original @forward
... │
5   │ @forward "../assets/bootstrap/bootstrap"; //bootstrap
    │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ new @forward
    ╵
  src/scss/foundation/_index.scss 5:1  @use
  src/scss/layout/_l-header.scss 3:1   @use
  src/scss/index.scss 8:1              root stylesheet

回避するためにはいくつかの方法が考えられますが、 Bootstrap 4 の @import を書き換えていくのは大変な上に既存ライブラリに手を入れるのはできれば避けたいので今回は不採用。

他の方法としては with を使う方法が考えられます。

ただし、これもいくつか工夫が必要です。

Dart Sass での検証2 (失敗 / @fowardwith)

安直に with を使おうかと考えましたが、構文的に with@use でしか使えません。

そのため、以下のような書き換えは不可です。

src/scss/foundation/_index.scss

@forward "../assets/bootstrap/bootstrap" with (
    /* with の中では use で読み込んだファイルのスコープは使えない */
    $theme-colors: (
        /* 上書き */
        primary: #333,
        /* デフォルトにないカラーの追加 */
        main: red,
    ),
); //bootstrap

Dart Sass での検証3 (失敗 / ファイルスコープ)

@use とセットで使うということが分かったので、今度は @use で読み込んでいる場所で with を付けたそうと思いました。

src/scss/layout/_l-header.scss

@use "../foundation" as f with (
    $theme-colors: (
        /* 上書き */
        primary: f.$own-color,
        /* デフォルトにないカラーの追加 */
        main: f.$own-main-color,
    ),
);

.l-header {
    .navbar-brand {
        &,
        &:link,
        &:visited {
            color: f.$own-main-color;
        }
        &:hover,
        &:active,
        &:focus {
            color: f.$own-main-color_l;
        }
    }
}

// 略

ただし、このような既述をすると以下のエラーが発生します。

Error: src/scss/layout/_l-header.scss
Error: There is no module with the namespace "f".
  ╷
7 │         primary: f.$own-color,
  │                  ^^^^^^^^^^^^^
  ╵
  src/scss/layout/_l-header.scss 7:18  @use
  src/scss/index.scss 8:1              root stylesheet

with の中では、その直前で既述した名前空間はまだ使えません。

「独自に定義したメインカラーでプライマリーカラーを上書きしたい」というケースが多いと思われるので、 with で上書きするときにメインカラーが変数で指定できないのは困ります。

そこで、独自定義の変数読み込みと Bootstrap 4 の読み込み部分を分けることにしました。

最終形

ディレクトリ構造

   /
  └ src/
    ├ html/
    │    └ index.html
    │
    └ scss/
       ├ assets/
       │  └ bootstrap/
       │    │    └ bootstrap/
       │    │         └ 略
       │    └ bootstrap.scss
       │
       ├ foundation/
       │    ├ _index.scss
       │    ├ _mixin.scss
       │    └ _variables.scss
       │
       ├ global/
       │    └ _index.scss      # 追加
       │
       ├ layout/
       │    ├ _l-footer.scss
       │    ├ _l-header.scss
       │    └ _l-main.scss
       │
       ├ object/
       │    ├ component/
       │    │    └ 略
       │    ├ project/
       │    │    └ 略
       │    └ utility/
       │         └ 略
       │
       └ index.scss

src/scss/foundation/_scss_variable.scss

先程まであった上書き用のコードは削除。

src/scss/foundation/_index.scss

@forward "variables";
@forward "mixin";

Bootstrap 4 を読み込む @forward を削除。

src/scss/global/_index.scss

@forward "../assets/bootstrap/bootstrap"; //bootstrap

Bootstrap 4 を読み込む部分を切り出したのを、 src/scss/global/_index.scss とします。

src/scss/layout/_l-header.scss

@use "../foundation" as f;
@use "../global" as g with (
    $theme-colors: (
        /* 上書き */
        primary: f.$own-color,
        /* デフォルトにないカラーの追加 */
        main: f.$own-main-color,
    ),
);

.l-header {
    .navbar-brand {
        &,
        &:link,
        &:visited {
            color: f.$own-main-color;
        }
        &:hover,
        &:active,
        &:focus {
            color: f.$own-main-color_l;
        }
    }
}

// 略

実際に使う部分。変数は foundation, Bootstrap 4 は global で読み込み、 foundation を先に読み込むことで、 with の中で使用できるようにしました。

src/scss/index.scss

@use "layout/l-header";
@use "layout/l-main";
@use "layout/l-footer";

index.css になる部分は読み込みだけ。

この形にすることでようやく当初意図していた形にすることができました。

備考1

今回の方法では Bootstrap 4 の中身は一切触れない方向で実装しました。そのため、 Bootstrap 4 の中は以前として @import で読み込まれており、変数はグローバルスコープに定義されるようです。

そのため、今回は src/scss/layout/_l-header.scss でしか with を使用していませんが……

デフォルト状態のサンプル
デフォルト状態のサンプル

デフォルト状態ではこのような状態になります(プライマリーカラーがデフォルト、右側はデフォルト状態では存在しない btn-main クラスが付与されているため背景色なし)。

適用状態のサンプル
適用状態のサンプル

src/scss/layout/_l-header.scsswith を適用しただけで、メイン部分のボタンも影響を受けます。今回の場合は「全体で色を変更したい」のでこれで良いのですが、 @use@forward の位置付けからすると役割を発揮できていない状態なので微妙なところ。

今後、 Bootstrap が @use, @forward に変更した場合にこの辺りの挙動は変わると思われます。

※ちなみに、v5.0.0-alpha3 でもまだ @import でした。

備考2

備考1と関連しますが、グローバルスコープに定義されるということは……

src/scss/layout/_l-header.scss

@use "../foundation" as f;
@use "../global" as g;

.l-header {
    .navbar-brand {
        &,
        &:link,
        &:visited {
            color: f.$own-main-color;
        }
        &:hover,
        &:active,
        &:focus {
            color: f.$own-main-color_l;
        }
    }
}

// 略

src/scss/layout/_l-header.scss は普通に src/scss/global/_index.scss を読み込み……

src/scss/layout/_l-main.scss

@use "../foundation" as f;
@use "../global" as g with (
    $theme-colors: (
        /* 上書き */
        primary: f.$own-color,
        /* デフォルトにないカラーの追加 */
        main: f.$own-main-color,
    ),
);

.l-main {
    background-color: f.$own-bg-color;
    color: f.$own-color;
    .btn-main {
        color: f.$own-color;
    }
}

// 略

src/scss/layout/_l-header.scss の後に読み込まれる src/scss/layout/_l-main.scsswith を使用した場合、以下のエラーが発生します。一度読み込んだモジュールが with 使用で再度読み込まれている、という旨のエラーですね。

Error: This module was already loaded, so it can't be configured using "with".
  ┌──> src/scss/layout/_l-main.scss
4 │ ┌ @use "../global" as g with (
5 │ │     $theme-colors: (
6 │ │         /* 上書き */
7 │ │         primary: f.$own-color,
8 │ │         /* デフォルトにないカラーの追加 */
9 │ │         main: f.$own-main-color,
10│ │     ),
11│ │ );
  │ └─^ new load
  ╵
  ┌──> src/scss/layout/_l-header.scss
4 │   @use "../global" as g;
  │   ━━━━━━━━━━━━━━━━━━━━━ original load
  ╵
  src/scss/layout/_l-main.scss 4:1  @use
  src/scss/index.scss 9:1           root stylesheet

Error: src/scss/layout/_l-main.scss
Error: This module was already loaded, so it can't be configured using "with".
  ┌──> src/scss/layout/_l-main.scss
4 │ ┌ @use "../global" as g with (
5 │ │     $theme-colors: (
6 │ │         /* 上書き */
7 │ │         primary: f.$own-color,
8 │ │         /* デフォルトにないカラーの追加 */
9 │ │         main: f.$own-main-color,
10│ │     ),
11│ │ );
  │ └─^ new load
  ╵
  ┌──> src/scss/layout/_l-header.scss
4 │   @use "../global" as g;
  │   ━━━━━━━━━━━━━━━━━━━━━ original load
  ╵
  src/scss/layout/_l-main.scss 4:1  @use
  src/scss/index.scss 9:1           root stylesheet

そのため、 with を使用するならば全体を通して src/scss/global/_index.scss が最初に @use で読み込まれる部分に既述する必要があります。


以上、知らないと全体的に嵌まり所が多い感じだったのでメモしておきます。

参考

with

今までのやり方 (参考)

Bootstrap (v5.0.0-alpha3)

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。