ダークモードにも柔軟に対応できるCSS&Sass変数のカラー設定方法

CSS変数とSass(Scss)変数を利用してダークモードになった時の配色などを自動で変更することができます。
例えばテーマボタンをクリックしたら<html>にJavaScriptでdata-theme="dark"という属性を追加し、そのdata属性を判定してダークモードの切り替えを実装する場合のCSS&Sassの設定を紹介します。

基本的なダークモードの設定方法

まずはライトモード(通常表示)用の$theme-colorsとダークモード用の$dark-theme-colorsというSass変数に配列で各カラーコードを定義していきます。
そしてSass機能の@each文で$theme-colors:root {}内にループ処理で出力していきます。
出力結果は--color-primary--color-secondaryのようにCSS変数で出力され、ダークモードの時は&[data-theme='dark'] {}内のカラーが適応されます。

/* Sass Variables */
// Light Theme Colors
$theme-colors: (
  primary: #333333,
  secondary: #cccccc,
  success: #16b43b,
  warning: #f8c11a,
  danger: #f13245,
  background: #e9e9e9,
  base: #f7f7f7,
);

// Dark Theme Colors
$dark-theme-colors: (
  primary: #f7f7f7,
  secondary: #cccccc,
  success: #33e65d,
  warning: #f1c339,
  danger: #f74254,
  background: #1a1a1a,
  base: #333333,
);

/* CSS Variables */
:root {
  // Light Mode
  @each $key, $color in $theme-colors {
    --color-#{$key}: #{$color};
  }

  // Dark Mode
  &[data-theme='dark'] {
    @each $key, $color in $dark-theme-colors {
      --color-#{$key}: #{$color};
    }
  }
}

これで基本的なダークモード対応したCSS設定は完了です。
var(--color-primary)のようにCSS変数var()関数で呼び出すことで、テーマに対応したカラーコードが自動で反映されます。

.wrapper {
  background-color: var(--color-wrapper);
}
.headding {
  color: var(--color-primary);
}

デモ

RGBAでカラー設定できない問題

上記の記述だとテーマごとにカラーを切り替えることは可能ですが、CSS変数だとRGBAで透過させたい時にrgba(var(--color-background, 0.5));のような記述は動作しません。

.overlay {
  // 動かない
  background-color: rgba(var(--color-background));
}

なぜrgba()関数が使えないのか

Sass環境だとrgba(#000000, 0.5);のように記述することができますが、
これはSass側がrgba()プロパティにHEX形式(#000000)の引数が入ったら自動でRGB形式(0, 0, 0)に変換してCSSにコンパイルしているから使用できるのです。
公式 Sass: color

しかし、CSS変数を呼び出すvar()関数はSassがCSSにコンパイルした後にしか動作しないので、var()関数をコンパイル前に読み込んでもrgba()に入った値はHEX形式からRGB形式に変換できません。

var() 関数は、プロパティ名、セレクター、またはプロパティ値以外のところでは使用できません。
(使用してしまうと、無効な構文が生成されるか、もしくはその変数に接続していない値が生成されてしまいます。)
引用:MDN

なのでrgba(var(--color-background, 0.5));のように記述してもSassがrgba()関数内でvar()関数を理解できないのでコンパイルできません。
(プロパティを使用していないvar()関数は使用できます)

.overlay {
  // 動かない(rgbaプロパティを使用している)
  background-color: rgba(var(--color-background), 0.5);
  // 動く(プロパティを使用してない)
  background-color: var(--color-background);
}

しかし、このままだとrgba()プロパティが使えないので、アルファを指定した透過処理ができなくなってしまいます。

それでもRGB形式のカラーコードは管理したくない

RGB形式のカラーコードは255, 255, 2550, 0, 0,のように3つ数字で構成されています。
これをそのままSass変数の$theme-colorsに設定しても良いんですが、デザインツール(Illustrator、Photoshop、Sketch、Figmaなど)を使用する場合HEX形式でカラーコードを取得することが多いので、新しいカラーを追加するにはデザインツール側でRGB形式に変換や取得する必要があります。

// デザインツール側でRGB形式のカラーコードを取得して追加する
$theme-colors: (
  primary: 51, 51, 51, // #333333
  secondary: 204, 204, 204, // #cccccc
  success: 22, 180, 59, // #16b43b
  warning: 248, 193, 26, // #f8c11a
  danger: 241, 50, 69, // #f13245
  background: 233, 233, 233, // #e9e9e9
  wrapper: 247, 247, 247, // #f7f7f7
);

これだとカラーコードだけで色の想像つきにくく、Visual Studio Codeエディタなどでカラー補完が効かない為、配色の管理が難しくなります。
(エディタや拡張機能によってはカラー補完が効く場合があります)

Sass環境でもCSS変数でrgba()プロパティを利用する為には?

上記を踏まえてSass変数はHEX形式のままCSS変数はRGB形式にしたいですね。
それを解決するためにはSass側でHEX形式のカラーコードをCSSにコンパイルする前にRGB形式に変換することで解決します。

  • Sass変数を定義(HEX形式)
  • Sass変数をRGB形式に変換 // ←ここ
  • CSS変数にSASS変数(RGB形式)を格納
  • CSS変数をvar()で呼び出す

HEX形式のカラーコードをRGB形式に変換するにはSassの@function機能で関数化するのが手っ取り早いです。
公式 Sass: @function

hexToRGB()関数を作成する

まずはhexToRGB()という関数を作成します。
これはhexToRGB()にHEX形式の値を渡したらred(), green(), blue()に分解しRGB形式のカラーコードに変換します。
そして:root{}内で#{hexToRGB($color)}で関数を呼び出すことで、Sassがコンパイルする前にRGB形式のカラーコードをCSS変数に出力します。

// HEX形式のカラーコードをRGBに変換する
@function hexToRGB($hex) {
  @return red($hex), green($hex), blue($hex);
}

:root {
  @each $key, $color in $theme-colors {
    --color-#{$key}: #{hexToRGB($color)}; // hexToRGB()関数を呼び出す
  }

  &[data-theme='dark'] {
    @each $key, $color in $dark-theme-colors {
      --color-#{$key}: #{hexToRGB($color)}; // hexToRGB()関数を呼び出す
    }
  }
}

これでrgb()プロパティでCSS変数を呼び出すことができますが、呼び出すごとにrgba(var(--color), 0.5)のような長い記述をしなければいません。

// alpha値が0.5(透過0.5)
.overlay {
  background-color: rgba(var(--color-background), 0.5);
}
// alpha値が1(透過なし)
.overlay {
  background-color: rgba(var(--color-background), 1);
}
// rgba()がなくても大丈夫
.overlay {
  background-color: var(--color-background);
}

getColor()関数を作成する

rgba()プロパティを使うたびに毎回rgba(var(--color), 0.5)と記述するのはめんどくさいので、カラーコード取得用のgetColor()という関数を作成します。

@function hexToRGB($hex) {
  @return red($hex), green($hex), blue($hex);
}

// CSS変数をRGBA関数で使えるようにする
@function getColor($color_name, $alpha: 1) { // $alphaはデフォルト値を入れておく(透明度)
  @return rgba(var(#{$color_name}), $alpha);
}

これでgetColor()を呼び出すだけで@function関数が実行され内部的にrgba(var(--color-background), 0.5)の形式に変換してくれます。

// alpha値が0.5(透過0.5)
.overlay {
  background-color: getColor(--color-background, 0.5);
}
// alpha値が1(透過なし)
.overlay {
  background-color: getColor(--color-background, 1);
}
// rgba()がなくても大丈夫
.overlay {
  background-color: var(--color-background);
}

完成

/* Functions */
// HEX形式のカラーコードをRGBに変換する
@function hexToRGB($hex) {
  @return red($hex), green($hex), blue($hex);
}

// CSS変数をRGBA関数で使えるようにする
@function getColor($color_name, $alpha: 1) {
  @return rgba(var(#{$color_name}), $alpha);
}

/* Sass Variables */
// Light Theme Colors
$theme-colors: (
  primary: #333333,
  secondary: #cccccc,
  success: #16b43b,
  warning: #f8c11a,
  danger: #f13245,
  background: #e9e9e9,
  base: #f7f7f7,
);

// Dark Theme Colors
$dark-theme-colors: (
  primary: #f7f7f7,
  secondary: #cccccc,
  success: #33e65d,
  warning: #f1c339,
  danger: #f74254,
  background: #1a1a1a,
  base: #333333,
);

/* CSS Variables */
:root {
  @each $key, $color in $theme-colors {
    --color-#{$key}: #{hexToRGB($color)};
  }

  &[data-theme='dark'] {
    @each $key, $color in $dark-theme-colors {
      --color-#{$key}: #{hexToRGB($color)};
    }
  }
}