Sass で色やブレークポイントを管理していると、$color-primary、$color-danger、$color-success…… と変数が延々と並んでいきます。数が増えるほど追加や変更が面倒になり、一覧で見渡すこともできません。こうしたときに役立つのが map というデータ型です。キーと値のペアをひとつの変数にまとめ、map.get() で必要な値だけを取り出せます。この記事では map の定義と取り出し方から、@each によるクラスの一括生成、ブレークポイントの管理、ネストした map の扱いまでを順に解説します。
目次
Sass の map とは
map は、キーと値のペアをいくつも持てるデータ型です。JavaScript のオブジェクトや PHP の連想配列にあたるものだと考えると分かりやすいでしょう。'primary' というキーに #0d6efd という色を、'danger' というキーに #dc3545 を、といった具合に対応づけて1つの変数に格納します。
map にまとめる利点は、値が増えても変数が増えないことだけではありません。map は @each で順番に取り出せるため、「登録されている色の分だけクラスを自動で書き出す」といった処理が書けるようになります。色を1つ足したいときに map へ1行追加するだけで、対応するクラスもメディアクエリもすべて付いてくる、という状態を作れるのが本当の利点です。
map を定義して map.get で取り出す
map の定義
丸括弧の中に キー: 値 をカンマ区切りで並べます。通常の変数と同じく $ から始まる名前を付けます。
$colors: (
'primary': #0d6efd,
'danger': #dc3545,
'gray': #6c757d, // 末尾のカンマは付けても付けなくてもよい
);
1行で $colors: ('primary': #0d6efd, 'danger': #dc3545); と書いても構いませんが、項目が増えることを見越して改行して並べるほうが読みやすく、差分も追いやすくなります。最後の項目のうしろにカンマを残しておくと、あとから行を追加するときにカンマの付け忘れが起きません。
キーはクォートで囲むのが一般的です。Sass ではクォート付きの文字列とクォートなしの文字列は等しいものとして扱われるため、'primary' で定義して primary で取り出しても値は返ってきます。ただし red や blue のような色名はクォートを外すと文字列ではなく色の値になってしまうので、キーは必ずクォートで囲むと決めておくのが安全です。
@use ‘sass:map’ を書いてから map.get を呼ぶ
値の取り出しには map.get() を使います。これは Sass の組み込みモジュール sass:map が提供する関数なので、使う前にファイルの先頭で @use 'sass:map'; を書いて読み込んでおく必要があります。読み込むと map という名前空間が使えるようになり、map.get() のように「名前空間.関数名」の形で呼び出せます。
@use 'sass:map'; // これを忘れると map.get が使えない
$colors: (
'primary': #0d6efd,
'danger': #dc3545,
);
.button {
background-color: map.get($colors, 'primary');
}
.button--danger {
background-color: map.get($colors, 'danger');
}
.button {
background-color: #0d6efd;
}
.button--danger {
background-color: #dc3545;
}
第1引数が対象の map、第2引数が取り出したいキーです。map そのものは CSS には出力されず、取り出した値だけが書き出されます。
map-get() は古い書き方
ネット上の記事では map-get($colors, 'primary') のようにハイフンでつないだ書き方をよく見かけます。これは @use が導入される前から使われてきたグローバル関数で、現在も動きますが非推奨です。コンパイルすると次のような警告が出ます。
DEPRECATION WARNING [global-builtin]: Global built-in functions are deprecated
and will be removed in Dart Sass 3.0.0.
Use map.get instead.
メッセージのとおり、グローバル関数は Dart Sass 3.0.0 で削除される予定です。これから書くコードでは @use 'sass:map'; を書いたうえで map.get() を使ってください。既存のコードも、書き換えておけば将来のバージョンアップで慌てずに済みます。
sass:map モジュールのよく使う関数
sass:map には get() 以外にも便利な関数が揃っています。実務でよく使うものをまとめます。$map は対象の map、$key はキーを表します。
| 関数 | 説明 |
|---|---|
map.get($map, $key) | キーに対応する値を返す。キーが無ければ null |
map.has-key($map, $key) | そのキーがあるかを true / false で返す |
map.keys($map) | キーだけを並べたリストを返す |
map.values($map) | 値だけを並べたリストを返す |
map.merge($map1, $map2) | 2つの map を統合した新しい map を返す。同じキーは後ろ側で上書きされる |
map.set($map, $key, $value) | 1つのキーだけを書き換えた新しい map を返す |
map.remove($map, $key) | 指定したキーを取り除いた新しい map を返す |
注意したいのは、map.merge() も map.set() も元の map を書き換えるわけではないという点です。どちらも変更後の新しい map を戻り値として返すので、結果を使いたい場合は $colors: map.set($colors, 'primary', #000); のように変数へ代入し直します。
@each と組み合わせてクラスを一括生成する
map の力がいちばん分かりやすく出るのがここです。@each $name, $color in $colors と書くと、キーが $name に、値が $color に入った状態で登録数だけ繰り返されます。クラス名にキーを埋め込むには #{$name} というインターポレーションを使います。
@use 'sass:map';
$colors: (
'primary': #0d6efd,
'danger': #dc3545,
'gray': #6c757d,
);
@each $name, $color in $colors {
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background-color: $color;
}
}
.text-primary {
color: #0d6efd;
}
.bg-primary {
background-color: #0d6efd;
}
.text-danger {
color: #dc3545;
}
.bg-danger {
background-color: #dc3545;
}
.text-gray {
color: #6c757d;
}
.bg-gray {
background-color: #6c757d;
}
色を1つ増やしたくなったら、$colors に1行足すだけで .text-* と .bg-* の両方が自動で増えます。手書きで6つのクラスを管理するのと比べて、書き漏らしや綴りの揺れが起きなくなるのが大きな違いです。
ブレークポイントを map にまとめてメディアクエリを作る
もうひとつの定番が、レスポンシブ対応のブレークポイント管理です。@media (min-width: 768px) という数値をあちこちに直接書いていると、値を変えたくなったときに全ファイルを探し回ることになります。map にまとめ、@mixin から map.get() で引く形にしておけば、変更は1か所で済みます。
@use 'sass:map';
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 992px,
);
@mixin mq($name) {
// 定義されていないキーならコンパイルを止めて知らせる
@if not map.has-key($breakpoints, $name) {
@error "ブレークポイント '#{$name}' は $breakpoints に定義されていません。";
}
@media (min-width: map.get($breakpoints, $name)) {
@content; // @include のブロックの中身がここに入る
}
}
あとは @include mq('md') { ... } と書くだけです。@content を使っているので、中括弧の中に書いたスタイルがそのままメディアクエリの中へ展開されます。
.card {
padding: 16px;
@include mq('md') {
padding: 32px;
}
}
.card {
padding: 16px;
}
@media (min-width: 768px) {
.card {
padding: 32px;
}
}
ここで入れた @error が効いてきます。@include mq('xl') のようにタイプミスをすると、静かに無視されるのではなく、次のようにメッセージ付きでコンパイルが止まります。間違いにすぐ気づけるので、共有するファイルほど入れておく価値があります。
Error: "ブレークポイント 'xl' は $breakpoints に定義されていません。"
ネストした map でテーマの値をまとめる
map の値には map を入れられます。「ボタンの背景色」「ボタンの文字色」のように部品ごとに値が分かれるときは、階層にまとめておくと見通しが良くなります。取り出すときは map.get() の第3引数以降にキーを続けて並べるだけです。
@use 'sass:map';
$theme: (
'button': (
'bg': #0d6efd,
'text': #fff,
),
'card': (
'bg': #ffffff,
'border': #dee2e6,
),
);
.button {
background-color: map.get($theme, 'button', 'bg');
color: map.get($theme, 'button', 'text');
}
.card {
background-color: map.get($theme, 'card', 'bg');
border: 1px solid map.get($theme, 'card', 'border');
}
map.get($theme, 'button', 'bg') は「$theme の 'button' を取り出し、その中の 'bg' を取り出す」という意味です。この多段の指定は Dart Sass の map.get() でサポートされているもので、map.get(map.get($theme, 'button'), 'bg') と入れ子に書く必要はありません。
存在しないキーを取ると null が返る
map.get() は、指定したキーが map に無くてもエラーにはなりません。代わりに null が返ります。そして Sass では値が null のプロパティは CSS に出力されません。
@use 'sass:map';
$colors: ('primary': #0d6efd);
.button {
color: map.get($colors, 'secondary'); // 存在しないキー
width: 10px;
}
.button {
width: 10px;
}
color の行がまるごと消えているのが分かります。エラーも警告も出ないため、キーを打ち間違えても「なぜかスタイルが当たらない」という形でしか気づけません。これが map を使ううえでいちばん厄介なつまずきどころです。
対策は2つあります。ひとつは前の章で書いたように @if not map.has-key(...) と @error を組み合わせ、キーが無ければ即座にコンパイルを止めてしまう方法です。もうひとつは、キーが無いときに既定値を使う方法で、@if map.has-key($colors, $name) で分岐して @else にフォールバックの値を書きます。他の人も触るファイルなら、黙って消えるより止まってくれるほうが結果的に早く直せます。
map.get でエラーになるとき
There is no module with the namespace “map”.
いちばん多いのがこのエラーです。@use 'sass:map'; を書き忘れたまま map.get() を呼ぶと、Sass は map という名前空間を知らないためコンパイルに失敗します。
Error: There is no module with the namespace "map".
╷
2 │ .a { color: map.get($colors, 'primary'); }
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
╵
気をつけたいのは、@use はファイル単位で効くという点です。パーシャルに分けている場合、_variables.scss で読み込んであっても、map.get() を書く側のファイルで書いていなければ同じエラーになります。map.get() を使うファイルにはそれぞれ @use 'sass:map'; が必要だと覚えておいてください。
@use rules must be written before any other rules.
@use は必ずファイルの先頭に書くというルールがあります。変数の定義やセレクターを書いたあとに @use 'sass:map'; を足すと、このエラーが出ます。あとから map.get() を使いたくなって、書いている場所の近くに @use を挿し込んでしまったときに起きがちです。コメントだけは @use より前にあっても問題ありません。
キーが合っているはずなのに値が取れない
エラーにならず null が返る場合は、キーの表記が一致していないことを疑います。まず、色名をクォート無しで書いたケースです。$colors: (red: 1); の red は文字列ではなく色の値として扱われるので、map.get($colors, 'red') では取り出せません。逆に 'red' で定義したものを red で引いても取り出せません。
もうひとつは、ハイフンとアンダースコアの違いです。Sass の変数名や mixin 名では $my-color と $my_color が同じものとして扱われますが、map のキーは単なる文字列なので、'text-color' と 'text_color' は別のキーになります。変数名の感覚で書き換えると値が取れなくなるので注意してください。心当たりがないときは @debug map.keys($colors); を挟むと、実際に登録されているキーの一覧をターミナルで確認できます。
まとめ
map はキーと値のペアをひとつの変数にまとめるデータ型で、色やブレークポイントのように「同じ種類の値が複数ある」ものの管理に向いています。値を取り出すときは、ファイルの先頭に @use 'sass:map'; を書いたうえで map.get($colors, 'primary') と呼びます。map-get() という古い書き方は非推奨で、将来のバージョンで削除される予定なので使わないでください。@each と組み合わせればクラスを一括生成でき、@mixin と組み合わせればブレークポイントを1か所で管理できます。ネストした map は map.get($theme, 'button', 'bg') のようにキーを並べて取り出せます。存在しないキーは null が返って静かに消えてしまうため、map.has-key() と @error でガードしておくと安心です。