Sass を使っていると、色やサイズをまとめる方法が2つあることに気づきます。Sass の $color-primary のような変数と、CSS のカスタムプロパティ(--color-primary と var())です。どちらも「値に名前を付けて使い回す」ための仕組みなので、どちらを使えばいいのか迷いがちです。
結論から言うと、この2つは動くタイミングが違います。Sass の変数はコンパイルのときに値へ置き換えられて消えてしまい、カスタムプロパティは CSS ファイルに残ってブラウザーが解決します。この違いが分かると、使い分けの基準がはっきりします。
この記事では、両者の性質を比べながら、どちらをどの用途に使うか、そして Sass の変数をカスタムプロパティに流し込む書き方までを解説します。
目次
$変数はコンパイル時に消える
まず Sass の変数から見ていきます。$color-primary と書いた箇所は、コンパイルの時点で実際の値に置き換えられます。出力された CSS に変数は残りません。
$color-primary: #007bff;
.button {
background-color: $color-primary;
}
.link {
color: $color-primary;
}
.button {
background-color: #007bff;
}
.link {
color: #007bff;
}
ブラウザーに届く時点では、ただの #007bff です。つまりブラウザー側から見ると、変数を使ったかどうかは分かりませんし、あとから値を差し替えることもできません。
カスタムプロパティは実行時に解決される
一方、CSS のカスタムプロパティはそのまま CSS に出力されます。値の解決はブラウザーが行うので、ページを表示したあとでも値を変えられます。
:root {
--color-primary: #007bff;
}
.button {
background-color: var(--color-primary);
}
:root {
--color-primary: #007bff;
}
.button {
background-color: var(--color-primary);
}
書いたものがほぼそのまま残っているのが分かります。カスタムプロパティは通常のプロパティと同じようにカスケードし、子要素へ継承されます。だからクラスを1つ付け替えるだけで、その要素以下の色をまとめて切り替えられます。
下のデモは、コンパイル後の CSS です。「テーマを切り替える」を押すと html 要素に .theme-dark が付き、--color-primary と --color-bg の値が入れ替わります。.panel や .button のスタイルには一切触れていない点に注目してください。
<div class="panel">
<p class="panel__text">テーマカラーを切り替えます</p>
<button class="button" type="button">ボタン</button>
</div>
<button class="switch" type="button">テーマを切り替える</button>
/* コンパイル後の CSS。値は var() で参照している */
:root {
--color-primary: #007bff;
--color-bg: #f5f7fa;
}
/* クラスが付いた瞬間、同じ var() の参照先が入れ替わる */
.theme-dark {
--color-primary: #ffb020;
--color-bg: #22262c;
}
.panel {
background-color: var(--color-bg);
padding: 24px;
border-radius: 8px;
transition: background-color 0.3s;
}
.panel__text {
color: var(--color-primary);
margin: 0 0 12px;
font-weight: bold;
}
.button {
background-color: var(--color-primary);
color: #fff;
border: none;
border-radius: 4px;
padding: 8px 20px;
cursor: pointer;
}
.switch {
margin-top: 16px;
padding: 6px 14px;
cursor: pointer;
}
const root = document.documentElement;
document.querySelector('.switch').addEventListener('click', () => {
// クラスを付け外しするだけで、var() が参照する値が切り替わる
root.classList.toggle('theme-dark');
});
同じことを Sass の変数でやろうとすると、.theme-dark .panel、.theme-dark .button …と、色を使っているセレクタの分だけ上書きを書く必要があります。値を後から差し替えられるかどうかが、実用面での一番大きな差です。
できること・できないことの比較
性質の違いを整理すると次のようになります。
| できること | Sass の $変数 | カスタムプロパティ var() |
|---|---|---|
| 値が解決されるタイミング | コンパイル時 | ブラウザーでの表示時 |
| 出力される CSS のサイズ | 増えない | 定義の分だけ増える |
| クラスの付け替えで値を変える | できない | できる |
| JavaScript から変更する | できない | できる |
| 子要素への継承・カスケード | されない(スコープは Sass のルール) | される |
| メディアクエリの条件に使う | 使える(@media (min-width: $bp)) | 使えない |
| Sass の色関数に渡す | 渡せる | 渡せない |
| 四則演算 | そのまま書ける | calc() が必要 |
| 古いブラウザー対応 | 出力は普通の CSS なので問題なし | IE では動かない |
表の下2つは、特につまずきやすいところです。カスタムプロパティは値が文字列として扱われるため、var(--space) * 2 のような書き方は無効です。calc(var(--space) * 2) と書きます。Sass の変数なら $space * 2 でそのまま計算できます。
また、@media (min-width: var(--bp-md)) は動きません。メディアクエリの条件部分はカスタムプロパティを解釈しないためです。ブレークポイントの管理は Sass の変数の得意分野です。
使い分けの目安
Sass の $変数が向いているもの
「書くときに決まっていて、あとから変わらない値」は Sass の変数が適しています。ブレークポイント、余白のスケール、z-index の段階、フォントのウェイトといった設計上の定数です。map にまとめて @each でクラスを量産する、といった Sass ならではの処理にも使えます。
// メディアクエリの条件に使うので Sass の変数で持つ
$bp-md: 768px;
$space-unit: 8px;
@mixin mq($width) {
@media (min-width: $width) {
@content;
}
}
.container {
padding: $space-unit * 2;
@include mq($bp-md) {
padding: $space-unit * 4;
}
}
カスタムプロパティが向いているもの
「表示中に変わる可能性のある値」はカスタムプロパティです。ライトモードとダークモードの色、ユーザー設定で変えられるアクセントカラー、JavaScript で計算してから流し込む値(スクロール量やヘッダーの高さなど)が代表例です。
// 実測した高さをカスタムプロパティとして渡す
const header = document.querySelector('.header');
document.documentElement.style.setProperty(
'--header-height',
`${header.offsetHeight}px`
);
// 現在の値を読み取ることもできる
const value = getComputedStyle(document.documentElement)
.getPropertyValue('--header-height');
コンポーネント単位で見た目を変えたいときにも便利です。.card の中で --card-padding を参照しておき、使う側が .card--compact { --card-padding: 8px; } と上書きする、という作り方ができます。
Sass の値をカスタムプロパティに渡す
実際のプロジェクトでは、両方を組み合わせるのが現実的です。値の一覧は Sass の変数や map で管理しておき、それを :root のカスタムプロパティとして書き出す、という形にすると管理場所を1か所にまとめられます。
このとき補間 #{} が必要です。カスタムプロパティの値は「そのまま出力される文字列」として扱われるため、補間せずに変数を書くと、変数名がそのまま CSS に出てしまいます。
$colors: (
"primary": #007bff,
"text": #333333,
"bg": #f5f7fa,
);
:root {
// これはNG。--color-primary: $value; と書くと
// 「$value」という文字列がそのまま出力されてしまう
// #{} で囲むと値として展開される
@each $name, $value in $colors {
--color-#{$name}: #{$value};
}
}
:root {
--color-primary: #007bff;
--color-text: #333333;
--color-bg: #f5f7fa;
}
なお、キー名の部分も --color-#{$name} のように補間できます。こうしておけば、値の管理は SCSS 側の $colors だけで済み、実際に使う場所では var(--color-primary) と書けます。ダークモードのときだけ --color-bg を上書きする、といった切り替えも自然に書けます。
組み合わせたときに引っかかるところ
Sass の関数に var() を渡すとエラーになる
color.adjust(var(--color-primary), $lightness: 10%) のような書き方はできません。Sass はコンパイル時に色を計算しますが、var(--color-primary) の中身はその時点では分からないためです。
実行時に色を調整したいなら、CSS 側の color-mix() を使うか、明るい色・暗い色をあらかじめ別のカスタムプロパティとして定義しておきます。Sass の色関数を使いたいなら、元の値は Sass の変数のまま扱ってください。
値が効かないときは単位と空白を疑う
カスタムプロパティは中身を検証しません。--space: 16(単位なし)を padding: var(--space) で使っても、単位がないので無効な宣言として無視されるだけで、エラーにもなりません。「なぜか効かない」という場合は、まず開発者ツールで実際に入っている値を確認してください。
参照先が未定義のときのために、var(--color-primary, #007bff) のようにカンマ区切りでフォールバック値を書いておくこともできます。
すべてをカスタムプロパティにすると重くなる
カスタムプロパティは CSS に残るので、定義した分だけファイルが大きくなります。切り替える予定のない値まで :root に並べると、出力が無駄に増えるうえ、どれが本当に動的なのか分かりにくくなります。「変わる値だけをカスタムプロパティにする」と決めておくと、この問題は起きません。
まとめ
Sass の $変数 はコンパイル時に展開されて消える値、CSS のカスタムプロパティは CSS に残ってブラウザーが解決する値です。この違いから、あとから変えられるかどうかという決定的な差が生まれます。
ブレークポイントや余白のスケール、@media の条件、Sass の関数に渡す値は $変数。テーマカラーのように表示中に切り替わる値、JavaScript から流し込む値、コンポーネント側で上書きしたい値は var()。この線引きで、ほとんどの場面は判断できます。
両方を使うときは、値の一覧を Sass の map で持ち、@each と補間 #{} で :root に書き出す形が扱いやすいです。管理場所は1つにまとめつつ、実行時の切り替えにも対応できます。