HTML5
CSS3
CSSDay 13

CSS カスタムプロパティはパラメータの夢を見るか

CSS カスタムプロパティ(CSS 変数)についてちょっと調べたことがあったので、この場を借りてまとめておきます。

はじめに

CSS はスタイルのプロパティ値を設定できても参照ができない。

たとえばボックスの height をテキストの行数で指定したくても、現在の line-height を参照することができないので calc() などで計算することはできず、あらかじめ算出した固定値を直接設定する必要がある。

CSS カスタムプロパティを使えば、意味付けされた値(変数)を定義し、複数のスタイルプロパティから参照したり、calc() での計算に用いることが可能になる。

.line-clump {
  /* 1行の高さ */
  --line-height: 1.5em;      /* 単位付きのプロパティ値 */
  /* 表示行数 */
  --line-count: 3;           /* 整数値 */

  /*リーダ用文字列を指定 */
  --leader: "…";             /* 文字列  (::before や ::after から参照できる) */

  line-height: var(--line-height);  /* 参照にはvar()を使う */
  /* ブロックの高さ(行高 x 行数) */
  height: calc(var(--line-count) * var(--line-height)); /* calcでの計算に使える*/
  width: 30em;
  overflow: hidden;
}

この値をスタイルシートの外部から与えたり動的に変更したい。

本稿では、CSS カスタムプロパティ(CSS 変数)を要素スタイルのパラメータ的に使えないものか、プログラマ視点で掘り下げる。

サンプルの動作は Chrome (63)で確認した。

CSS カスタムプロパティの仕様の確認というより、ほとんど Chrome 実装の動作検証になっているレベルなので、ほかのブラウザではどうなるのか分からない。

個別要素へのカスタムプロパティ注入

CSS カスタムプロパティは「CSS変数」とも呼ばれるが、プログラマ的には「変数」というより「オーバーライド可能な定数」と言ったほうがしっくりくる。

カスタムプロパティも、通常のスタイルプロパティと同様に、要素階層をカスケードする。

従って、ある要素で定義したカスタムプロパティは子要素と弟要素から参照することがきる。

  #hoge {
    --color: red
  }

  /* 子要素 */
  #hoge span {
    color: var(--color);
  }

  /* 弟要素 */
  #hoge ~ p {
    color: var(--color);
  }

さらに、下位要素で同じカスタムプロパティ名を定義することで値を上書き(オーバーライド)することができる。

要素の style 属性で直接カスタムプロパティを定義すれば、要素への CSS パラメータ を与えているかのように使うことができる。

#color-circle.plate {
  position: relative;
  width: calc(2 * var(--r));
  height: calc(2 * var(--r));
  display: flex;
  justify-content: center; 
}

#color-circle.plate > .item {
  position: absolute;
  width: 60px;
  height: 60px;
  --angle: calc(calc(var(--i) * 360) / var(--n));
  background-color: hsl(var(--angle), 100%, 50%);
  transform-origin: 50% var(--r);
  transform: rotate(calc(var(--angle) * 1deg));
}
<div id="color-circle" class="plate" style="--r:200px; --n:12">
  <div class="item" style="--i:1"></div>
  <div class="item" style="--i:2"></div>
  <div class="item" style="--i:3"></div>
  <div class="item" style="--i:4"></div>
  <div class="item" style="--i:5"></div>
  <div class="item" style="--i:6"></div>
  <div class="item" style="--i:7"></div>
  <div class="item" style="--i:8"></div>
  <div class="item" style="--i:9"></div>
  <div class="item" style="--i:10"></div>
  <div class="item" style="--i:11"></div>
  <div class="item" style="--i:12"></div>
</div>

colors.png

style 属性でインデックスを指定するのはちょっとダサい。
できれば要素数や位置インデックスから値を算出できたいが、CSS だけではできない。
せめて data 属性を使いたいが、attr() も役に立たない。

もちろん、JavaScript を使えばどうにでもなる。

// 要素数からパラメータ値を算出し、カスタムパラメータとして設定する
var num = $("#color-circle > .item").length;
var radius = $("#color-circle").width() / 2;
$("#color-circle").css({"--r":radius+"px", "--n":num+""});
$("#color-circle > .item").each(function(i){
  $(this).css({"--i":i+""});
});
var num = 24;
var radius = 320;
$("#color-circle").css({"--r":radius+"px", "--n":num+""});
for (var i=0; i<num; i++) {
  // 要素を追加してしまう
  $("#color-circle").append($("<div>")
     .addClass("item")
     .css({"--i":i+""}));
}
<div id="color-circle" class="plate"><!--空 --></div>

環境に依存したカスタムプロパティの切り替え

基本となるスタイルシートからカスタムプロパティだけを@importなどで外出しにすれば、それを設定ファイルのようにして、スタイルやテーマを環境によって制御するようなこともできる。

config.css
:root {
  --debug-box: block;
}
/* 設定 css の読み込み */
@import "config.css";

/* デバックモードで可視となるクラス */
.debug-box {
  display: var(--debug-box, none);
  color: red;
  border: 2px dotted red;
}

言語やメディアタイプでスタイルを切り替えるのも何かに使えるかもしれない。

/* langによって表示内容を切り替える */
nav:lang(ja) { 
  --back: "戻る";
  --next: "次へ"; 
}
nav:lang(en) { 
  --back: "Back"; 
  --next: "Next"; 
}

nav .back::after {
  content: var(--back);
}
nav .next::after {
  content: var(--next);
}
<nav lang="ja">
  <a class="back" href="#"></a> <a class="next" href="#"></a>
</nav>
/* スクリーンではtitle属性の内容を表示させる */
@media screen {
  :root {
    --link-assist: title;
  }
}
/* 印刷ではhref属性のURLを表示させる */
@media print {
  :root {
    --link-assist: href;
  }
}
#comment a::after {
  content: "(" attr(var(--link-assist)) ")";
}
<div id="comment">
  <p>『好きなサイトは<a href="https://qiita.com" title="Qiita" target="_blank">ここ</a>です!』</p>
</div>

ユーザアクションによる動的スタイル変更

フォームコントロール

状態を持つ要素でカスタムプロパティを定義すると、スタイルを動的に変更できる。
ただ、CSS は親要素をセレクトできないので、カスタムプロパティ値を使える状況が限られる。

チェックボックスやラジオボタンは親要素をもたないので :checked セレクタで弟要素にプロパティをカスケードすることができる。

<div id="rgb01">
  <input type="checkbox" id="r"><label for="r">Red</label>
  <input type="checkbox" id="g"><label for="g">Green</label>
  <input type="checkbox" id="b"><label for="b">Blue</label>
  <div class="rgb"></div>
</div>
#r:checked ~ .rgb {--r: 255}
#g:checked ~ .rgb {--g: 255}
#b:checked ~ .rgb {--b: 255}

.rgb {
  background-color: rgb(var(--r, 0), var(--g, 0), var(--b, 0));
}
  • 【デモ】 RGB混色
    チェックボックスの :checked セレクタでカスタムプロパティ値を組み合わせて、RGB値を動的に変更する。 対象の要素はチェックボックスの兄弟要素でなければならない。

    See the Pen qVewpX by kumazo (@kumazo) on CodePen.

しかし SELECT の OPTION ではそうはいかない。OPITON 要素は SELECT 要素を必ず親に持つので、カスタムプロパティを持たせてもその外の要素から参照できない。

このような場合 JavaScript を使わざるを得ない。

  フォント:
  <select id="font-family" onchange="changeFont(this)">
    <option>serif</option>
    <option>sans-serif</option>
    <option>cursive</option>
    <option>fantasy</option>
    <option>monospace</option>
  </select>

  フォントサイズ:
  <select id="font-size" onchange="changeFont(this)">
    <option>10pt</option>
    <option>12pt</option>
    <option selected>16pt</option>
    <option>20pt</option>
    <option>24pt</option>
  </select>
function changeFont(sel) {
  /* カスタムプロパティを設定するにはsetProperty()を使う */
  document.getElementById("font-sample")
    .style.setProperty('--' + sel.id, sel.value);
}
#font-sample p {
  font-family: var(--font-family, serif);
  font-size: var(--font-size, 16pt);
}

CSS アニメーション

カスタムプロパティの値を動的に変更できれば、animation を制御するのに使える。

animation の設定を変更するほか、 @keyframes ごと差し替えたり、要素別のカスタムプロパティを @keyframes 内で参照することができる。

/* ラジオボタンやチェックボックスで変数の値を切り替える */
・・・
#normal:checked       ~ .box {--direction : normal}
#reverse:checked      ~ .box {--direction : reverse}
#alternate:checked    ~ .box {--direction : alternate}
#alternate-reverse:checked ~ .box {--direction : alternate-reverse}
・・・
#running:checked      ~ .box {--play-state : running}
#paused:checked       ~ .box {--play-state : paused}
・・・

@keyframes act {
  0% {
    background: hsl(0, 100%, 50%);
  }
  100% {
    /* 要素スタイルで定義したカスタムプロパティを参照する */
    background: var(--color);
    transform : var(--transform);
  }
}

.box {
・・・

  /* animation の設定がカスタムプロパティの値の変更によって動的に反映されsされる */
  animation-name : var(--name, act);
  animation-duration : var(--duration, 0s);
  animation-timing-function : var(--timing-function, ease);
  animation-delay : var(--delay, 0s);
  animation-iteration-count : var(--iteration-count, 1);
  animation-direction : var(--direction, normal);
  animation-fill-mode : var(--fill-mode, none);
  animation-play-state : var(--play-state, running);
}

/* 要素ごとにパラメータを設定 */
#box01 {
  --color : hsl(240, 100%, 50%);
  --transform : rotate(360deg);
}
・・・
@keyframes a01 {
   0% {transform: translate(5px)}
  50% {transform: translate(-5px)}
 100% {transform: translate(5px)}
}
@keyframes a02 {
   0% {transform: scale(1.05)}
  50% {transform: scale(0.95)}
 100% {transform: scale(1.05)}
}
・・・

#a01:active ~ .box {--name: a01}
#a02:active ~ .box {--name: a02}
・・・

#panel .box {
・・・

  animation: var(--name, none) 100ms linear infinite;
}

  • 【デモ】 ハコをふるわせる
    CSS カスタムプロパティで animation の @keyframes を切り替える

    See the Pen VywZqR by kumazo (@kumazo) on CodePen.

逆に、@keyframes 内でカスタムプロパティを定義して要素のプロパティをフレームごとに切り替えることもできる。

@keyframes inc {
    0% {--i: 0} 
   10% {--i: 1}
   20% {--i: 2}
   30% {--i: 3}
   40% {--i: 4}
   50% {--i: 5}
   60% {--i: 6}
   70% {--i: 7}
   80% {--i: 8}
   90% {--i: 9}
  100% {--i:10}
}

.runnable {
  animation : inc 10s infinite linear both;
}

#charge::before {
  ・・・
  width : calc(var(--i) * 10%);
  ・・・
}

JavaScript

JavaScript からカスタムプロパティに値を設定することで、CSS パラメータとして要素スタイルを動的に制御すすことができる。

$(function(){
  $(".ripple").on('mousedown', function(e) {
    var x = e.offsetX;
    var y = e.offsetY;
    var r = $(this).width();
    $(this).css({
      "--x": x + "px", 
      "--y": y + "px",
      "--r": r + "px"
    });
  });
});
  • 【デモ】 マテリアル リップル
    Chrome では JavaScript の mousedown イベントで設定したカスタムプロパティが CSS の :active 擬似クラスで拾えているので、マウス操作で要素スタイルを制御できる。

See the Pen KZJVjz by kumazo (@kumazo) on CodePen.

代替スタイルシート

「代替スタイルシート」というのは聞きなれないかもしれないが、ブラウザ側でスタイルシートを切り替えられるようにするための HTML 仕様で、今はほとんど忘れ去られている。

基本スタイルシートで参照するカスタムプロパティを代替スタイルシートに定義すれば、ユーザがメニューからデザインテーマを変更することができる。

代替スタイルシートの構成は LINK か STYLE のtitle 属性で行う。

<!-- 先頭のSTYLEがでおフォルト -->
<style title="デフォルト">
  :root {
    --fg-color: black;
    --bg-color: white;
    --accent-color: red;
  }
</style>
<style title="カラフル">
  :root {
    --fg-color: blue;
    --bg-color: ivory;
    --accent-color: green;
  }
</style>

ただし残念ながら、Chrome は代替スタイルシートをサポートしない。
Firefox なら今でもサポートしているので、インストールしている人は試してみてほしい。

alternate.png

その他気付き

  • url()関数の引数では var()が使えない。
  • カスタムプロパティの定義で、同名のカスタムプロパティは使用できない。これが変数とは違うところだ。
#hoge {
  --color: var(--color, red); /* エラー */
}
  • デバッグが難しい。
    Chrome の Developer Tools でカスタムプロパティの変数の値は展開されないので、calc() などが挟まると実際の最終的な値がわからない。
    文字列と数値ならかろうじてcontentで表示させることができるが、それも苦しい。
#hoge01 {
  --string: "test";
  --int: 123;
}

  /* 文字列 */
#hoge01::after {
  content: var(--string);
}

 /* 数値 */
#hoge01::after {
  counter-reset: c var(--int);
  content: counter(c);
}

デモ

  • 【デモ】アナログ時計デザイン
    新旧の2つのアナログ時計のスタイルの定義は共通で、デザインだけカスタムプロパティで変更している

    See the Pen mpvXoy by kumazo (@kumazo) on CodePen.


  • 【デモ】 ローディングアニメ
    カスタムプロパティで animation の初期状態やタイミングをずらす

    See the Pen ZvEzJe by kumazo (@kumazo) on CodePen.


まとめ

間に合わなかったでござる。

参考