余白のユーティリティクラスを .mt-1 から .mt-10 まで手で書いたり、色違いのボタンを5種類ぶん同じ形でコピーしたり。CSS を書いていると、こうした「ほとんど同じだけど数字や名前だけが違う」記述をひたすら並べる作業が出てきます。Sass にはこれを自動化するための制御構文があり、@each でリストを回し、@for で決まった回数だけ繰り返し、@if で条件によって出力を変えられます。この記事では、それぞれの書き方とコンパイル後の CSS、クラス名を組み立てるのに欠かせない #{}(補間)、そしてループが思ったとおりに展開されないときの確認どころまでを解説します。記法は SCSS を前提にしています。
目次
手で並べていたクラスを Sass に書かせる
制御構文(コントロールディレクティブ)は、Sass のコンパイル時にだけ動く仕組みです。ブラウザーが実行するわけではなく、コンパイルが終わった時点では普通の CSS になっています。つまり「CSS を手で書く代わりに、CSS を書き出すルールを書く」という発想です。5行の SCSS が50行の CSS になる、というのが制御構文のいちばん分かりやすい効果です。
手で書き並べていると、ひとつだけ数値を打ち間違える、あとから刻み幅を変えたくなって全部直す、といった事故が起きます。ループにしてしまえば、直す場所は元になるリストや回数の指定だけです。まずは4つの構文の役割を整理しておきます。
| 構文 | 何をするか | よく使う場面 |
|---|---|---|
@each | リストやマップの要素をひとつずつ取り出して繰り返す | 色名・サイズ名・アイコン名など、名前が決まっているものの一括生成 |
@for | 開始の数から終了の数まで、数値を1ずつ増やして繰り返す | 余白ユーティリティ、グリッドの列幅、連番のアニメーション遅延 |
@if / @else | 条件が真のときだけブロックの中身を出力する | 引数によって出力を変える mixin、値が未指定のときの処理 |
@while | 条件が真であるあいだ繰り返す | 回数が単純な連番にならないとき(使う場面は少ない) |
#{} の補間でクラス名を組み立てる
ループを使う前に、先に補間(インターポレーション)を押さえておきます。#{$変数} という書き方で、変数の中身をその場の文字として埋め込む機能です。margin: $size; のようにプロパティの値として変数を使うときは補間は不要ですが、セレクタ名やプロパティ名の一部として変数を使うときは補間が必須です。
$name: 'primary';
$side: 'top';
// セレクタ名に埋め込む
.btn-#{$name} {
// プロパティ名に埋め込む
margin-#{$side}: 8px;
}
.btn-primary {
margin-top: 8px;
}
クォート付きの文字列を補間すると、クォートは外れた状態で埋め込まれます。.btn-#{$name} が .btn-'primary' にならないのはこのためです。ループで作るクラス名はほぼ必ずこの形になるので、「変数を名前の一部にするときは #{} で包む」と覚えておいてください。
@each|リストやマップをひとつずつ回す
@each $変数 in リスト { ... } と書くと、リストの要素が先頭から順に変数へ入り、その回数だけブロックが展開されます。名前が決まっているものを並べて作るのがいちばん得意です。
リストを回す
@each $name in (sm, md, lg) {
.icon-#{$name} {
background-image: url('/images/icon-#{$name}.svg');
}
}
.icon-sm {
background-image: url("/images/icon-sm.svg");
}
.icon-md {
background-image: url("/images/icon-md.svg");
}
.icon-lg {
background-image: url("/images/icon-lg.svg");
}
リストはカンマ区切りでもスペース区切りでも構いません。丸かっこも省略できるので、@each $name in sm, md, lg と書いても同じです。読みやすさのためにかっこを付けておくと、あとから要素を足すときに範囲が分かりやすくなります。
マップを回してキーと値を同時に受け取る
名前と値をセットで管理したいときは、マップ(キーと値の組)を @each $key, $value in $map の形で回します。変数を2つ書いておくと、キーと値が別々の変数に入ります。
$theme-colors: (
'primary': #0d6efd,
'success': #198754,
'danger': #dc3545,
);
@each $name, $color in $theme-colors {
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background-color: $color;
}
}
.text-primary {
color: #0d6efd;
}
.bg-primary {
background-color: #0d6efd;
}
.text-success {
color: #198754;
}
.bg-success {
background-color: #198754;
}
.text-danger {
color: #dc3545;
}
.bg-danger {
background-color: #dc3545;
}
13行ほどの SCSS が6ブロックの CSS になりました。色を1つ足したいときは、マップに1行追加するだけで .text-* と .bg-* の両方が増えます。マップそのものの扱いは別記事で詳しく触れているので、ここでは @each で回す材料として使っています。
複数の値をまとめて分解して受け取る
要素がそれ自体リストになっている場合も、変数を並べて書けば分解して受け取れます。1件あたり3つ以上の情報を持たせたいときに便利です。
// 「名前 文字色 背景色」の3点セットを並べたリスト
$alerts:
(info #055160 #cff4fc),
(warning #664d03 #fff3cd),
(error #58151c #f8d7da);
@each $name, $text, $bg in $alerts {
.alert-#{$name} {
color: $text;
background-color: $bg;
}
}
受け取る変数の数が要素の数より多いときは、足りない分に null が入ります。エラーにはならず静かに空になるので、リストの長さはそろえておくのが安全です。
@for|決まった回数だけ数値を増やして繰り返す
@for は数値を1ずつ増やしながら繰り返します。書き方は2種類あり、終わりの数を含めるかどうかが違います。
| 書き方 | 意味 | $i に入る値 |
|---|---|---|
@for $i from 1 through 5 | 終わりの数を含む | 1, 2, 3, 4, 5(5回) |
@for $i from 1 to 5 | 終わりの数を含まない | 1, 2, 3, 4(4回) |
「5個ほしい」と思ったときに書くべきなのは through 5 のほうです。to は「5の手前まで」という意味になります。英語の through(〜まで通して)と to(〜へ)の違いだと考えると覚えやすいでしょう。なお、開始の数より終了の数が小さい場合(@for $i from 5 through 1)は、1ずつ減りながら繰り返します。
余白のユーティリティクラスを作る
$space-unit: 8px;
@for $i from 1 through 4 {
.mt-#{$i} {
margin-top: $space-unit * $i;
}
}
.mt-1 {
margin-top: 8px;
}
.mt-2 {
margin-top: 16px;
}
.mt-3 {
margin-top: 24px;
}
.mt-4 {
margin-top: 32px;
}
クラス名の #{$i} は補間が必要ですが、margin-top の値は $space-unit * $i という計算式をそのまま書けます。基準の余白を8pxから4pxに変えたくなっても、直すのは $space-unit の1行だけです。
nth-child でアニメーションの開始をずらす
リストの項目を少しずつ遅らせて表示する、という演出も @for の定番です。:nth-child() のかっこの中はセレクタの一部なので、ここでも補間が必要になります。
.fade-list > li {
opacity: 0;
animation: fade-in 0.4s ease forwards;
@for $i from 1 through 3 {
&:nth-child(#{$i}) {
animation-delay: $i * 0.1s;
}
}
}
.fade-list > li {
opacity: 0;
animation: fade-in 0.4s ease forwards;
}
.fade-list > li:nth-child(1) {
animation-delay: 0.1s;
}
.fade-list > li:nth-child(2) {
animation-delay: 0.2s;
}
.fade-list > li:nth-child(3) {
animation-delay: 0.3s;
}
セレクタの内側にそのまま @for を書ける点にも注目してください。制御構文はトップレベルでもセレクタの中でも使えるので、関係するスタイルの近くにループを置いておけます。
@if / @else if / @else|条件によって出力を変える
@if 条件 { ... } は、条件が真のときだけ中身を出力します。続けて @else if と @else を書けば、複数の分岐を作れます。いちばん活きるのは、引数で見た目を切り替える mixin の中です。
@mixin button($variant: filled, $color: #0d6efd) {
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
@if $variant == filled {
background-color: $color;
color: #fff;
border: none;
} @else if $variant == outline {
background-color: transparent;
color: $color;
border: 1px solid $color;
} @else {
background-color: transparent;
color: $color;
border: none;
}
}
.btn-primary {
@include button;
}
.btn-outline {
@include button(outline);
}
.btn-primary {
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
background-color: #0d6efd;
color: #fff;
border: none;
}
.btn-outline {
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
background-color: transparent;
color: #0d6efd;
border: 1px solid #0d6efd;
}
選ばれなかった分岐の中身は、CSS にはまったく出力されません。条件はコンパイル時に一度だけ評価され、結果だけが書き出されるからです。ブラウザー側で判定が行われるわけではない、という点は CSS の @media との大きな違いです。
条件には ==(等しい)、!=(等しくない)のほか、数値どうしなら < や >= も使えます。複数の条件をつなぐときは、記号ではなく and / or / not という英単語を使います。
Sass で「偽」になるのは false と null だけ
ここは他の言語の感覚でいると間違えやすいところです。Sass では false と null の2つだけが偽で、それ以外はすべて真として扱われます。JavaScript や PHP では偽になる 0 や空文字列も、Sass では真です。
| 値 | @if での扱い |
|---|---|
false | 偽 |
null | 偽 |
0 | 真 |
''(空文字列) | 真 |
()(空のリスト・マップ) | 真 |
そのため「引数が指定されていなければ出力しない」という判定には、初期値を null にしておくのが定石です。0 を初期値にすると、指定がなくても条件が真になってしまいます。
@mixin card($radius: null) {
padding: 16px;
background-color: #fff;
// $radius が渡されたときだけ border-radius を出力する
@if $radius {
border-radius: $radius;
}
}
なお、値が null のプロパティはそもそも CSS に出力されません。border-radius: null; と書いた行は丸ごと消えるので、単純な出し分けであれば @if を使わずに済むこともあります。
@while は条件が真のあいだ繰り返す
4つ目の @while は、条件が真であるあいだブロックを繰り返します。カウンター用の変数を自分で用意し、ループの中で更新する必要があります。
$i: 1;
@while $i <= 4 {
.w-#{$i} {
width: 25% * $i;
}
$i: $i + 1; // 更新を忘れると無限ループになる
}
この例は @for $i from 1 through 4 でそのまま書けるので、あえて @while を選ぶ理由はありません。実際の場面でも、増え方が単純な連番でないときなど限られたケースでしか出番がなく、公式ドキュメントでも @each や @for のほうが読みやすいと案内されています。まずは @each と @for を使いこなし、どうしても書けないときの逃げ道として @while を覚えておけば十分です。
生成される CSS の量は思っているより増える
ループは便利ですが、出力される CSS の量は「繰り返す回数 × ブロックの中の宣言の数」で増えていきます。書く側の手間が増えないぶん、増えていることに気づきにくいのが厄介なところです。
たとえば余白のユーティリティを、上下左右とその組み合わせの6種類、刻みを10段階、さらにブレークポイント3つぶん生成したとします。ルールの数は6×10×3で180個です。1ルールあたり2行としても360行、実際にはセレクタや波かっこも含めてそれ以上になります。ループを1つ足しただけのつもりでも、掛け算で効いてくるわけです。
対策としては、実際に使う範囲だけを生成する(1 through 20 を 1 through 8 に減らす)、ブレークポイントごとの生成は本当に必要なものに絞る、といった調整が基本になります。未使用の CSS を削るツールを導入する手もありますが、まずは「そのループは何ルール生むのか」を意識して書くことが大切です。ネットワーク上のサイズは gzip などの圧縮でかなり小さくなるとはいえ、ブラウザーが解釈するルールの数そのものは減りません。
ループが思ったとおりに展開されないとき
制御構文でつまずくパターンはだいたい決まっています。コンパイルは通るのに CSS がおかしい、というケースが多いので、出力された CSS を実際に開いて確認するのが近道です。
クラス名が「$i」のまま出力される
最頻出は補間の書き忘れです。.mt-$i { ... } と書くと、Sass は $i を変数ではなくただの文字として扱い、.mt-$i というセレクタをそのまま出力します。エラーにならないぶん見逃しやすく、ブラウザーの開発者ツールで「クラスが当たっていない」と気づいてから原因を探すことになりがちです。セレクタやプロパティ名の中で変数を使うときは #{$i} と包む、と徹底してください。
繰り返しの回数が1つ足りない・1つ多い
to と through の取り違えです。.mt-5 を作ったつもりが .mt-4 までしかない、というときは @for $i from 1 to 5 になっていないか確認します。他の言語のループでは「終端を含まない」ほうが一般的なので、そちらに慣れているほど間違えやすいところです。逆に 0 から始めて to を使うと、@for $i from 0 to 5 で0から4までの5回になります。どちらの書き方でも構いませんが、プロジェクト内では統一しておくと読み違いが減ります。
マップを回したのにキーと値が分かれない
マップを @each $item in $theme-colors のように変数1つで受け取ると、$item には「キーと値が入った2要素のリスト」がまとめて入ります。そのまま color: $item; と書くと color: "primary" #0d6efd; のような妙な値が出力されてしまいます。マップを回すときは @each $name, $color in $theme-colors と、受け取る変数を2つ書くのが正解です。
同じ理屈で、先ほどの3点セットのリストを2変数で受け取ると3つ目が捨てられます。リストの要素数と、受け取る変数の数はそろえてください。
プロパティの値のほうで補間が要るケース
プロパティの値では基本的に補間は不要ですが、例外があります。代表的なのが CSS カスタムプロパティ(-- で始まる変数)です。カスタムプロパティの値は Sass から見るとただのテキストとして扱われ、SassScript が評価されません。次のように補間して初めて、計算結果が入ります。
:root {
@for $i from 1 through 3 {
--space-#{$i}: #{$i * 8px}; // 値の側にも #{} が必要
}
}
:root {
--space-1: 8px;
--space-2: 16px;
--space-3: 24px;
}
ここで補間を外して --space-#{$i}: $i * 8px; と書くと、CSS には --space-1: $i * 8px; という文字列がそのまま出てしまいます。
逆に、普通のプロパティで補間を使いすぎるのも考えものです。font-size: #{$i}px; のように書くと、結果は数値ではなく「クォートなしの文字列」になり、そのあとの計算に使えなくなります。単位を付けたいだけなら $i * 1px と掛け算するのが正しい書き方です。値の側では補間を使わず、計算式をそのまま書く、と覚えておいてください。
まとめ
Sass の制御構文を使うと、似たようなクラスを手で並べる作業をコンパイル時の処理に任せられます。@each はリストやマップを回す構文で、@each $key, $value in $map のように変数を並べればキーと値を同時に受け取れます。@for は数値を1ずつ増やす構文で、through は終わりの数を含み、to は含まないという違いがいちばんの注意点です。@if / @else if / @else は条件で出力を切り替える構文で、Sass では false と null だけが偽になり 0 や空文字列は真になる、という独自の挙動を押さえておく必要があります。@while もありますが、出番は多くありません。そして、これらでクラス名を組み立てるには #{} による補間が欠かせません。クラス名が $i のまま出ていたら補間の付け忘れ、個数が1つずれていたら to と through の取り違えを疑ってください。最後に、短い SCSS が大量の CSS になるという性質も忘れずに、必要な範囲だけを生成するようにしましょう。