CSS を JS で拡張したいという思いとアイデアと現状

CSSJavaScript で拡張できないかと思っていた.デザイナーは簡単に表現できることが増えるし,エンジニアは保守性が上がってよさそうだな,ということを考えていた.

ところで

こういう提案はすでにあって,ブラウザの API 経由で CSS を拡張できる experimental な実装があるようだ.CSS Houdini と呼ばれている.各ブラウザの実装状況は Is Houdini Ready Yet? というサイトで確認できる:

ishoudinireadyyet.com

ちなみに Houdini は フーディニ と発音するようで,どういう意味か調べたら An escape artist という意味らしい引田天功みたいな人ってことかな.信憑性が疑わしい.

CSS Houdini

CSS Houdini がどういう使い方ができるか調べてみたら,Chrome が先行していろいろ実装していてデモが確認できる https://googlechromelabs.github.io/houdini-samples/

Layout API は JS 側で要素のレイアウトを構成したり,Paint APICanvas で要素を自由にレンダーできるようになっている.特に Paint API の例にある QR コードの例はおもしろい:
https://googlechromelabs.github.io/houdini-samples/paint-worklet/qr-code/

f:id:mangano-ito:20200512204020p:plain
QR コードが表示される <textarea>

<textarea> だけど QR コードがレンダーされている謎な状況になっている.エンコードするデータは --text カスタムプロパティによって CSS に渡されている:

github.com

地味に重要な機能として Properties & Values API というのがある.CSS のカスタムプロパティ (--prop) の型をJS から定義できる.JS から CSS にデータを渡すのに使えるし,CSS アニメーションでは補完が効くようになったりするのが嬉しいポイントになっている.後に出てくる試みでもふんだんに使っている.

CSS Houdini+

ところで,CSS Houdini では関数やセレクタの拡張は現時点では特に見込みがなさそうだった.Parser API はそれを試みようとしていそうだが,ステータスを見てもわかるようにまだまだ構想段階という感じがしている.なので,CSS メタプログラミングに傾倒してめちゃくちゃカスタマイズしたいという野望は当面実現しがたい.

これを現時点でがんばるなら,CSSプリプロセスして変更した CSS + それのヘルパー JS を emit する仕組みが必要と思った.webpack とか parcel でひとまとめにできればそこまで煩わしくもない気もする.polyfill みたいにブラウザネイティブな API で同じことができるようになったとき,ネイティブ実装に移譲できるようになっているとか.と考えたところで babel を思い出して考えるのをやめた.

CSS を拡張したいという試み

さて,もし CSS Houdini のようなかたちで CSS のプロパティや関数,セレクタが拡張できるとしたらどうだろう.自分が思いついた「僕の考えたクールな CSS の拡張の提案」と現時点での実装をした.多くは CSS Houdini の機能を使っていて Chrome でないと完全な動作はしない代物になっている.

(※ 以下はすべてあったらいいなという妄想の構文なので,実際には使えないということを注意されたい)

::nth-letter(i) 疑似要素

指定した要素のある位置の文字を修飾するための疑似要素の提案:

<p id="my-paragraph">This is a paragraph.</p>
p#my-paragraph::nth-letter(2n + 1) {
    color: red;
    text-transform: uppercase;
}

たとえば この例はこういう結果を得る:

f:id:mangano-ito:20200512232408p:plain
こういう感じ

なんの役に立つのか,というとクラシックな Web サイトでよく使われていた意外とマークアップがめんどうくさい
文字を表現したい,という需要にも応えることができる.

今できる範囲で実現するとこうなる:

See the Pen :nth-letter(expression) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/NWGMvBq

この例は適当だけど,実際は他の兄弟要素があっても影響が出ないように,Shadow DOM にしたり,innerHTML で雑に置き換えたりしないように考慮しないといけないとか,そういう課題が山積みになっている.

ちなみに Chrome だと CSS Houdini の成果によりレインボー文字がアニメーションするようになっている. Firefox では動かないのでプログレッシブ・エンハンスメントという名の未対応になっている:

f:id:mangano-ito:20200512195928g:plain
きれい

このアニメーションは先述した Properties & Values API でカスタムプロパティに型を定義することで実現している.そうすると値を @keyframes0360 に無限ループで連続的に変化させることができるので,hsl の色相が 0 度 〜 360 度でトランジションするようになっている:

:root {
    --angle: 0;
    animation: angle_around 1s linear 0s infinite;
}

@keyframes angle_around {
      0% { --angle: 0; }
    100% { --angle: 360; }
}

#letter {
    $h: calc(var(--angle) * 1deg);
    color: hsl($h, 75%, 50%);
}

dataset(selector) 関数

<element data-prop="1234" /> において rule: dataset(prop); で対応する data-prop 属性の値を使う関数.

attr(data-*) すればいいのでは,という話題があるけど,attrcontent 以外の多くのプロパティで使えない感じがしている.あまねくプロパティで属性値の変化に応じてリアクティブにスタイルが変わってほしい.型は文脈によって推論されたい.

そうすれば,たとえばプログレスバー的なコンポーネントに対して data-progress に進捗度合いを設定することによって,CSS だけでプログレスバーのアニメーションを実現できたりするだろう:

<p id="progress" data-progress="0.2">In Progress...</p>
#progress {
    $color: hsl(100, 50%, 50%);
    $progress: calc(dataset(progress) * 100%);
    background-image: linear-gradient(
        to right,
        $color      0%,
        $color      $progress,
        transparent $progress,
        transparent 100%
    );
    transition: background-image 0.25s ease-in-out 0.1s;
}

f:id:mangano-ito:20200512224031g:plain
よくあるプログレスバー

現時点でこれを実現するためには JS を経由してカスタムプロパティを設定する.例によって Properties & Values API が使えない Firefox では動かない:

See the Pen dataset(selector) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/gOazXov

ここでは MutationObserverdata-progress の変化をウォッチして,変化があったら --prop--progress カスタムプロパティに設定するようになっている.JS → HTML → JS → CSS という遠回りな値のバケツリレーになっている.

scroll-y プロパティ

これは単にビューポートのスクロールした値が渡ってくるプロパティ.Android<CoordinatorLayout> みたいなネイティブアプリのプロフィール画面でよくあるパララックスっぽい効果とかに活用できると思った:

これは現時点でも単に --scroll-ywindow.scrollY を渡せばできる.

See the Pen CoordinatorLayout by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/PoPeear

例の理由で Firefox ではパララックスにはならないけど読める.requestAnimationFrame でガチャガチャやっているので重い.ちなみに画像は CodePen で用意されている Assets 使っている.

f:id:mangano-ito:20200512230700g:plain
Jane さんのプロフィール

しかしながら HTML と TypeScript はシンプルだが,まあ SCSS はとても汚い書き方になっている.Dev Tools のアニメーションのタイムラインを見るといかにも重そうな感じになっている:

f:id:mangano-ito:20200512234256p:plain
Dev Tools > Animations

CSS 側でいろいろ計算をやろうとするとカオスになってくる.スクロールの振る舞いはやっぱり JS で計算したほうがいいのでは,となって議論が一周してきたりした.

randomOf(values...) 関数

これは与えられた values... からランダムに値を選択するための関数:

p {
    color: randomOf(red, blue, yellow);
}

こうすると #my-class はページをリロードするたびに red, blue, yellow のどれかが毎回ランダムに選択されるようになる.こういう感じ:

See the Pen random_of(...values) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

めちゃくちゃ地味だけど,これは業務で実際に実装する必要があったから欲しいと思っている人はいるはず.JS 側で動的に style を変更したり,パターンごとに複数のクラスを生やして JS 側でクラスを与える必要がなくなる.それだけといえばそれだけ.

:within-viewport 疑似セレクタ

この疑似セレクタは 要素がビューポートに入っていれば有効になるセレクタだ.現時点では InsersectionObserver を使ってクラスを当てることで実現している:

See the Pen :within-viewport by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/oNjdore

またもや地味.これくらいはわざわざ専用のセレクタにしなくても,普通に JS で class を設定されるほうが素直な気がしている.

感想

意見がまとまらなかったので箇条書きにした:

  • CSS でデータからプレゼンテーションの要素を分離するコンセプトは素晴らしい
  • CSS を拡張できたら,JS はビジネスロジックのデータだけを扱えば済むし,デザインを変えたいときは CSS だけを変更するので済むのでは.(そんなに甘くないかな)
  • 結局,現時点でも JS で style をゴリゴリすれば大抵のことは実現できるので充分とも言える
  • カオスにならない程度に CSS を拡張できると面白そう