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


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

# はじめに

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

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

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

```css
.line-clump {
  /* １行の高さ */
  --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;
}
```
* 【デモ】 **三点リーダ**  
<p data-height="265" data-theme-id="0" data-slug-hash="OOdrWb" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="OOdrWb" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/OOdrWb/">OOdrWb</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

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

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

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

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

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

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

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

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

```css
  #hoge {
    --color: red
  }

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

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

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

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

```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));
}
```
```html
<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](https://qiita-image-store.s3.amazonaws.com/0/21239/27ae1b27-f2aa-360b-fc25-f668cc7c28d3.png)


* [色相環（style属性）](https://codepen.io/kumazo/pen/MONpoK) - CodePen

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

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

```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+""});
});
```
* [色相環（要素数から算出）](https://codepen.io/kumazo/pen/EoxYvb) -- CodePen

```javascript
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+""}));
}
```
```html
<div id="color-circle" class="plate"><!--空 --></div>
```
* [色相環（動的生成）](https://codepen.io/kumazo/pen/YEmmNY) - CodePen


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

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


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

/* デバックモードで可視となるクラス */
.debug-box {
  display: var(--debug-box, none);
  color: red;
  border: 2px dotted red;
}
```
* [デバッグモード表示](https://codepen.io/kumazo/pen/aEvwGq) - CodePen

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

```css
/* 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);
}
```
```html
<nav lang="ja">
  <a class="back" href="#"></a> <a class="next" href="#"></a>
</nav>
```
* [言語環境による文言の差し替え](https://codepen.io/kumazo/pen/eyYOEW) - CodePen

```css
/* スクリーンではtitle属性の内容を表示させる */
@media screen {
  :root {
    --link-assist: title;
  }
}
/* 印刷ではhref属性のURLを表示させる */
@media print {
  :root {
    --link-assist: href;
  }
}
#comment a::after {
  content: "(" attr(var(--link-assist)) ")";
}
```
```html
<div id="comment">
  <p>『好きなサイトは<a href="https://qiita.com" title="Qiita" target="_blank">ここ</a>です！』</p>
</div>
```
* [メディアタイプに依存したリンク表示の差し替え](https://codepen.io/kumazo/pen/OjgKLK) - CodePen

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

## フォームコントロール

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

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

```html
<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>
```
```css
#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値を動的に変更する。
対象の要素はチェックボックスの兄弟要素でなければならない。  
<p data-height="265" data-theme-id="0" data-slug-hash="qVewpX" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="qVewpX" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/qVewpX/">qVewpX</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

  

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

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

```html
  フォント:
  <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>
```
```javascript
function changeFont(sel) {
  /* カスタムプロパティを設定するにはsetProperty()を使う */
  document.getElementById("font-sample")
    .style.setProperty('--' + sel.id, sel.value);
}
```
```css
#font-sample p {
  font-family: var(--font-family, serif);
  font-size: var(--font-size, 16pt);
}
```
* 【デモ】 **フォントの変更**  
<p data-height="265" data-theme-id="0" data-slug-hash="RjXOoM" data-default-tab="html,result" data-user="kumazo" data-embed-version="2" data-pen-title="RjXOoM" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/RjXOoM/">RjXOoM</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

## CSS アニメーション

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

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

```css
/* ラジオボタンやチェックボックスで変数の値を切り替える */
・・・
#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);
}
・・・
```
* 【デモ】[CSS animation の動作確認](https://codepen.io/kumazo/pen/pdXYVB) - CodePen


```css
@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` を切り替える  
<p data-height="265" data-theme-id="0" data-slug-hash="VywZqR" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="VywZqR" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/VywZqR/">VywZqR</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>



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

```css
@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%);
  ・・・
}
```

* 【デモ】**@keyframes カウンタ**  
<p data-height="265" data-theme-id="0" data-slug-hash="PEVmpY" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="PEVmpY" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/PEVmpY/">PEVmpY</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

## JavaScript

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

```javascript
$(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` 擬似クラスで拾えているので、マウス操作で要素スタイルを制御できる。

<p data-height="500" data-theme-id="0" data-slug-hash="KZJVjz" data-default-tab="css" data-user="kumazo" data-embed-version="2" data-pen-title="KZJVjz" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/KZJVjz/">KZJVjz</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

## 代替スタイルシート

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

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

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

```HTML
<!-- 先頭の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>
```
* [代替スタイルシート](https://codepen.io/kumazo/pen/NwQZoB) - CodePen

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

![alternate.png](https://qiita-image-store.s3.amazonaws.com/0/21239/36659934-575e-7b95-c40e-2d8ee6a9dca1.png)

# その他気付き

* `url()`関数の引数では `var()`が使えない。
* カスタムプロパティの定義で、同名のカスタムプロパティは使用できない。これが変数とは違うところだ。

```css
#hoge {
  --color: var(--color, red); /* エラー */
}
```

* デバッグが難しい。  
Chrome の Developer Tools でカスタムプロパティの変数の値は展開されないので、calc() などが挟まると実際の最終的な値がわからない。  
文字列と数値ならかろうじて`content`で表示させることができるが、それも苦しい。

```css
#hoge01 {
  --string: "test";
  --int: 123;
}

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

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

# デモ

* 【デモ】**アナログ時計デザイン**
新旧の２つのアナログ時計のスタイルの定義は共通で、デザインだけカスタムプロパティで変更している
<p data-height="400" data-theme-id="0" data-slug-hash="mpvXoy" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="mpvXoy" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/mpvXoy/">mpvXoy</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

* 【デモ】 **ローディングアニメ**
カスタムプロパティで animation の初期状態やタイミングをずらす
<p data-height="265" data-theme-id="0" data-slug-hash="ZvEzJe" data-default-tab="css,result" data-user="kumazo" data-embed-version="2" data-pen-title="ZvEzJe" class="codepen">See the Pen <a href="https://codepen.io/kumazo/pen/ZvEzJe/">ZvEzJe</a> by kumazo (<a href="https://codepen.io/kumazo">@kumazo</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script>

# まとめ

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

# 参考
* CSS Custom Properties for Cascading Variables Module Level 1  
https://www.w3.org/TR/css-variables/
* CSS カスケード変数のためのカスタムプロパティ — CSS Custom Properties for Cascading Variables Module Level 1 （日本語訳）  
https://triple-underscore.github.io/css-variables-ja.html
* CSSの変数を使う - CSS | MDN  
(https://developer.mozilla.org/ja/docs/Web/CSS/Using_CSS_variables)
* CSS Variables\(カスタムプロパティ\)： なぜ、関心を持つべきか？ | プログラミング | POSTD  
http://postd.cc/css-variables-why-should-you-care/
* hsl\(\)とcalc\(\)、そしてCustom Propertiesを使った色の生成 - Hail2u  
https://hail2u.net/blog/generating-colors-with-hsl-calc-and-custom-properties.html
* Making Custom Properties (CSS Variables) More Dynamic | CSS-Tricks  
https://css-tricks.com/making-custom-properties-css-variables-dynamic/
* そのほか CSS-Tricks 内のカスタムプロパティ記事  
https://css-tricks.com/?s=custom+properties
* CSS Variables\(カスタムプロパティ\)でCSSがより便利に！ - Qiita  
https://qiita.com/kyota/items/bd5d291809415cc2d7b1
* CSS Variables(カスタムプロパティ) は CSS Animation と使うと幸せ - Qiita  
https://qiita.com/ygkn/items/33abcc3a5621bdf77c8d
* CSS カスタムプロパティと JavaScript Event - Qiita  
https://qiita.com/TSHiYK/items/ce72062235e48f3bdef8
* [jQuery]でcssプロパティを動的に追加、変更、削除する - Qiita  
https://qiita.com/kazTera/items/ab5dd9fb5b2579b25c4d
* CSSアニメーション 入門 - Qiita  
https://qiita.com/soarflat/items/4a302e0cafa21477707f