カスケードのCSSカスタムプロパティ
公開: 2022-03-10先月、Twitterで、「スコープ」スタイル(ビルドプロセスで生成)と「ネスト」スタイルのCSSネイティブの違いについて話し合いました。 逸話的に、開発者がJavaScriptによって生成された「スコープスタイル」を採用しながら、IDセレクターの特異性を回避する理由を尋ねました。 キース・グラントは、違いはカスケード*と継承のバランスを取ること、つまり特異性よりも近接性を優先することにあると示唆しました。 見てみましょう。
カスケード
CSSカスケードは、次の3つの要素に基づいています。
-
!important
フラグとスタイルの起源によって定義される重要性(ユーザー>作成者>ブラウザー) - 使用するセレクターの特異性(インライン> ID>クラス>要素)
- コード自体のソース順序(最新のものが優先されます)
近接性についてはどこにも言及されていません—セレクターのパーツ間のDOMツリーの関係。 2番目の段落の#inner p
は#outer p
よりも密接な関係を示していますが、以下の段落は両方とも赤になります。
<section> <p>This text is red</p> <div> <p>This text is also red!</p> </div> </section>
#inner p { color: green; } #outer p { color: red; }
両方のセレクターは同じ特異性を持ち、両方とも同じp
要素を記述しており、どちらも!important
としてフラグが立てられていないため、結果はソース順序のみに基づいています。
BEMとスコープスタイル
BEM(“ Block__Element—Modifier”)のような命名規則は、カスケードを完全に回避して、各段落が1つの親のみに「スコープ」されるようにするために使用されます。 段落「要素」には、「ブロック」コンテキストに固有の固有のクラスが与えられます。
<section class="outer"> <p class="outer__p">This text is red</p> <div class="inner"> <p class="inner__p">This text is green!</p> </div> </section>
.inner__p { color: green; } .outer__p { color: red; }
これらのセレクターは、依然として同じ相対的な重要性、特異性、およびソースの順序を持っていますが、結果は異なります。 「スコープ付き」または「モジュラー」CSSツールは、そのプロセスを自動化し、HTMLに基づいてCSSを書き直します。 以下のコードでは、各段落はその直接の親にスコープされています。
<section outer-scope> <p outer-scope>This text is red</p> <div outer-scope inner-scope> <p inner-scope>This text is green!</p> </div> </section>
p[inner-scope] { color: green } p[outer-scope] { color: red; }
継承
近接性はカスケードの一部ではありませんが、CSSの一部です。 そこで、継承が重要になります。 セレクターからp
を削除すると、各段落は最も近い祖先から色を継承します。
#inner { color: green; } #outer { color: red; }
#inner
と#outer
はそれぞれ異なる要素、つまりdiv
とsection
を記述しているため、両方の色のプロパティが競合することなく適用されます。 ネストされたp
要素には色が指定されていないため、結果はカスケードではなく継承(直接の親の色)によって決定されます。 近接性が優先され、#inner値が#inner
をオーバーライドし#outer
。
しかし、問題があります。継承を使用するために、 section
とdiv
内のすべてをスタイリングしています。 段落の色を具体的にターゲットにします。
(再)カスタムプロパティの紹介
カスタムプロパティは、ブラウザネイティブの新しいソリューションを提供します。 それらは他のプロパティと同じように継承しますが、定義されている場所で使用する必要はありません。 命名規則やビルドツールを使用せずにプレーンCSSを使用すると、カスケードよりも近接性を優先して、ターゲットとコンテキストの両方のスタイルを作成できます。
p { color: var(--paragraph); } #inner { --paragraph: green; } #outer { --paragraph: red; }
カスタムの--paragraph
プロパティは、 color
プロパティと同じように継承しますが、その値を適用する方法と場所を正確に制御できるようになりました。 --paragraph
プロパティは、直接選択(特異性ルール)またはコンテキスト(近接ルール)のいずれかを介してp
コンポーネントに渡すことができるパラメーターと同様に機能します。
これは、関数、ミックスイン、またはコンポーネントに関連付けられることが多いカスタムプロパティの可能性を示していると思います。
カスタムの「関数」とパラメータ
関数、ミックスイン、およびコンポーネントはすべて同じアイデアに基づいています。再利用可能なコードであり、さまざまな入力パラメーターを使用して実行し、一貫性がありながら構成可能な結果を得ることができます。 違いは、結果をどのように処理するかにあります。 縞模様の勾配変数から始めて、それを他の形式に拡張できます。
html { --stripes: linear-gradient( to right, powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
その変数はルートhtml
要素で定義されているため( :root
を使用することもできますが、不必要な特異性が追加されます)、ストライプ変数はドキュメント内のどこでも使用できます。 グラデーションがサポートされている場所ならどこにでも適用できます。
body { background-image: var(--stripes); }
パラメータの追加
関数は変数のように使用されますが、出力を変更するためのパラメーターを定義します。 --stripes
変数を更新して、その中にいくつかのパラメーターのような変数を定義することで、より関数のようにすることができます。 まず、 to right
をvar(--stripes-angle)
に置き換えて、角度を変更するパラメーターを作成します。
html { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
関数が提供する目的に応じて、作成できる他のパラメーターがあります。 ユーザーが独自のストライプカラーを選択できるようにする必要がありますか? もしそうなら、私たちの関数は5つの異なる色パラメーターを受け入れますか、それとも現在のように外に出る3つだけを受け入れますか? カラーストップのパラメータも作成しますか? 追加するすべてのパラメーターは、単純さと一貫性を犠牲にして、より多くのカスタマイズを提供します。
そのバランスに対する普遍的な正しい答えはありません。一部の機能はより柔軟である必要があり、他の機能はより意見を述べる必要があります。 コードの一貫性と可読性を提供するために抽象化が存在するため、一歩下がって、目標が何であるかを尋ねてください。 本当にカスタマイズ可能である必要があるのは何ですか?また、一貫性をどこに適用する必要がありますか? 場合によっては、完全にカスタマイズ可能な1つの関数ではなく、2つの意見のある関数を使用した方が便利な場合があります。
上記の関数を使用するには、 --stripes-angle
パラメーターの値を渡し、その出力をbackground-image
のようなCSS出力プロパティに適用する必要があります。
/* in addition to the code above… */ html { --stripes-angle: 75deg; background-image: var(--stripes); }
継承とユニバーサル
習慣からhtml
要素に--stripes
関数を定義しました。 カスタムプロパティは継承します。関数をどこでも使用できるようにしたいので、ルート要素に配置するのは理にかなっています。 これは、 --brand-color: blue
ような変数を継承する場合にうまく機能するため、「関数」でも機能することを期待できます。 ただし、ネストされたセレクターでこの関数を再度使用しようとすると、機能しません。
div { --stripes-angle: 90deg; background-image: var(--stripes); }
新しい--stripes-angle
は完全に無視されます。 再計算が必要な関数の継承に依存できないことがわかりました。 これは、各プロパティ値が要素(この場合はhtml
ルート要素)ごとに1回計算され、計算された値が継承されるためです。 ドキュメントルートで関数を定義することにより、関数全体を子孫が利用できるようにするのではなく、関数の計算結果のみを使用できるようにします。
これは、カスケード--stripes-angle
パラメーターの観点からフレーム化する場合に意味があります。 継承されたCSSプロパティと同様に、子孫は使用できますが、祖先は使用できません。 ネストされたdiv
に設定した値は、 html
ルートの祖先で定義した関数では使用できません。 任意の要素で再計算する普遍的に利用可能な関数を作成するには、すべての要素でそれを定義する必要があります。
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); }
ユニバーサルセレクターを使用すると、関数をどこでも使用できるようになりますが、必要に応じて、より狭く定義することもできます。 重要なことは、明示的に定義されている場所でのみ再計算できるということです。 いくつかの選択肢があります:
/* make the function available to elements with a given selector */ .stripes { --stripes: /* etc… */; } /* make the function available to elements nested inside a given selector */ .stripes * { --stripes: /* etc… */; } /* make the function available to siblings following a given selector */ .stripes ~ * { --stripes: /* etc… */; }
これは、継承に依存しない任意のセレクターロジックで拡張できます。
自由パラメーターとフォールバック値
上記の例では、 var(--stripes-angle)
には値もフォールバックもありません。 呼び出される前に定義またはインスタンス化する必要があるSassまたはJS変数とは異なり、CSSカスタムプロパティは定義せずに呼び出すことができます。 これにより、コンテキストから継承できる関数パラメーターに似た「自由」変数が作成されます。
最終的には、 html
または:root
(または他の祖先)で変数を定義して継承された値を設定できますが、値が定義されていない場合は、最初にフォールバックを考慮する必要があります。 必要な動作に応じて、いくつかのオプションがあります
- 「必須」パラメーターの場合、フォールバックは必要ありません。 現状では、関数は
--stripes-angle
が定義されるまで何もしません。 - 「オプション」パラメーターの場合、
var()
関数でフォールバック値を指定できます。 変数名の後にコンマを追加し、その後にデフォルト値を追加します。
var(--stripes-angle, 90deg)
各var()
関数はフォールバックを1つだけ持つことができるため、追加のコンマはその値の一部になります。 これにより、内部コンマを使用して複雑なデフォルトを提供できます。
html { /* Computed: Hevetica, Ariel, sans-serif */ font-family: var(--sans-family, Hevetica, Ariel, sans-serif); /* Computed: 0 -1px 0 white, 0 1px 0 black */ test-shadow: var(--shadow, 0 -1px 0 white, 0 1px 0 black); }
ネストされた変数を使用して独自のカスケードルールを作成し、さまざまな値にさまざまな優先順位を付けることもできます。
var(--stripes-angle, var(--global-default-angle, 90deg))
- まず、明示的なパラメーター(
--stripes-angle
);を試してください。 - 使用可能な場合は、グローバルな「ユーザーデフォルト」(
--user-default-angle
)にフォールバックします。 - 最後に、「工場出荷時のデフォルト」
(90deg
)にフォールバックします。
カスタムプロパティを明示的に定義するのではなく、 var()
でフォールバック値を設定することにより、パラメーターに特異性やカスケード制限がないことを確認します。 すべての*-angle
パラメータは、どのコンテキストからも継承できるように「自由」です。
ブラウザのフォールバックと可変のフォールバック
変数を使用する場合、覚えておく必要のある2つのフォールバックパスがあります。
- 変数をサポートしていないブラウザでは、どの値を使用する必要がありますか?
- 特定の変数が欠落しているか無効である場合、変数をサポートするブラウザーはどの値を使用する必要がありますか?
p { color: blue; color: var(--paragraph); }
古いブラウザは変数宣言プロパティを無視し、 blue
にフォールバックしますが、最近のブラウザは両方を読み取り、後者を使用します。 var(--paragraph)
は定義されていない可能性がありますが、有効であり、前のプロパティをオーバーライドするため、変数をサポートするブラウザーは、 unset
キーワードを使用する場合と同様に、継承された値または初期値にフォールバックします。
それは最初は紛らわしいように思えるかもしれませんが、それには十分な理由があります。 1つ目は技術的です。ブラウザエンジンは「解析時」(最初に発生)で無効または不明な構文を処理しますが、変数は「計算値時間」(後で発生)まで解決されません。
- 解析時に、無効な構文を持つ宣言は完全に無視されます—以前の宣言にフォールバックします。 これは、古いブラウザがたどるパスです。 最近のブラウザは変数構文をサポートしているため、代わりに前の宣言が破棄されます。
- 計算値の時点で、変数は無効としてコンパイルされていますが、遅すぎます—前の宣言はすでに破棄されています。 仕様によると、無効な変数値は
unset
と同じように扱われます。
html { color: red; /* ignored as *invalid syntax* by all browsers */ /* - old browsers: red */ /* - new browsers: red */ color: not a valid color; color: var(not a valid variable name); /* ignored as *invalid syntax* by browsers without var support */ /* valid syntax, but invalid *values* in modern browsers */ /* - old browsers: red */ /* - new browsers: unset (black) */ --invalid-value: not a valid color value; color: var(--undefined-variable); color: var(--invalid-value); }
これは、変数をサポートするブラウザーのより複雑なフォールバックを試して、古いブラウザーの単純なフォールバックを提供できるため、作成者としても役立ちます。 さらに良いことに、これにより、 null
/ undefined
の状態を使用して必要なパラメーターを設定できます。 これは、関数をミックスインまたはコンポーネントに変換する場合に特に重要になります。
カスタムプロパティ「Mixins」
Sassでは、関数は生の値を返しますが、ミックスインは通常、プロパティと値のペアを使用して実際のCSS出力を返します。 ユニバーサル--stripes
プロパティを定義すると、それを視覚的な出力に適用せずに、結果は関数のようになります。 出力を普遍的に定義することで、それをミックスインのように動作させることができます。
* { --stripes: linear-gradient( var(--stripes-angle), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
--stripes-angle
が無効または未定義のままである限り、ミックスインはコンパイルに失敗し、 background-image
は適用されません。 いずれかの要素に有効な角度を設定すると、関数は計算して背景を提供します。
div { --stripes-angle: 30deg; /* generates the background */ }
残念ながら、そのパラメータ値は継承されるため、現在の定義ではdiv
とすべての子孫に背景が作成されます。 これを修正するには、すべての要素で--stripes-angle
値をinitial
値(または無効な値)に置いて、継承しないようにする必要があります。 同じユニバーサルセレクターでそれを行うことができます:
* { --stripes-angle: initial; --stripes: /* etc… */; background-image: var(--stripes); }
安全なインラインスタイル
場合によっては、バックエンドサーバーまたはフロントエンドフレームワークからのデータに基づいて、CSSの外部から動的にパラメーターを設定する必要があります。 カスタムプロパティを使用すると、通常の特異性の問題を気にすることなく、HTMLで変数を安全に定義できます。
<div>...</div>
インラインスタイルは特異性が高く、オーバーライドするのが非常に困難ですが、カスタムプロパティを使用する場合は、無視するという別のオプションがあります。 divをbackground-image: none
(たとえば)そのインライン変数は影響を与えません。 さらに詳しく説明するために、中間変数を作成できます。
* { --stripes-angle: var(--stripes-angle-dynamic, initial); }
これで、HTMLで--stripes-angle-dynamic
を定義するか、無視して、スタイルシートで直接--stripes-angle
を設定するかを選択できます。
プリセット値
より複雑な値、または再利用したい一般的なパターンについては、以下から選択できるいくつかのプリセット変数を提供することもできます。
* { --tilt-down: 6deg; --tilt-up: -6deg; }
そして、値を直接設定するのではなく、これらのプリセットを使用します。
<div>...</div>
これは、動的データに基づいてチャートやグラフを作成したり、手帳をレイアウトしたりするのに最適です。
コンテキストコンポーネント
また、明示的なセレクターに適用し、パラメーターをオプションにすることで、「ミックスイン」を「コンポーネント」として再構成することもできます。 --stripes-angle
の存在または不在に依存して出力を切り替えるのではなく、コンポーネントセレクターの存在または不在に依存することができます。 これにより、フォールバック値を安全に設定できます。
[data-stripes] { --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
フォールバックをvar()
関数内に配置することにより、 --stripes-angle
を未定義のままにして、コンポーネントの外部から値を継承するために「自由」にすることができます。 これは、コンポーネントスタイルの特定の側面をコンテキスト入力に公開するための優れた方法です。 JSフレームワークによって生成された(またはSVGアイコンのようにshadow-DOM内でスコープされた)「スコープ付き」スタイルでさえ、このアプローチを使用して、外部の影響に対する特定のパラメーターを公開できます。
分離されたコンポーネント
継承のためにパラメーターを公開したくない場合は、デフォルト値で変数を定義できます。
[data-stripes] { --stripes-angle: to right; --stripes: linear-gradient( var(--stripes-angle, to right), powderblue 20%, pink 20% 40%, white 40% 60%, pink 60% 80%, powderblue 80% ); background-image: var(--stripes); }
これらのコンポーネントは、クラスやその他の有効なセレクターでも機能しますが、 data-
属性を選択して、必要な修飾子の名前空間を作成しました。
[data-stripes='vertical'] { --stripes-angle: to bottom; } [data-stripes='horizontal'] { --stripes-angle: to right; } [data-stripes='corners'] { --stripes-angle: to bottom right; }
セレクターとパラメーター
データ属性を使用して変数を設定したい場合がよくあります。これは、CSS3 attr()
仕様でサポートされている機能ですが、どのブラウザーにもまだ実装されていません(各ブラウザーのリンクされた問題については、[リソース]タブを参照してください)。 これにより、セレクターを特定のパラメーターとより密接に関連付けることができます。
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
<div data-stripes="30deg">...</div> /* Part of the CSS3 spec, but not yet supported */ /* attr( , ) */ [data-stripes] { --stripes-angle: attr(data-stripes angle, to right); }
それまでの間、 style
属性を使用して同様のことを実現できます。
<div>...</div> /* The `*=` atttribute selector will match a string anywhere in the attribute */ [style*='--stripes-angle'] { /* Only define the function where we want to call it */ --stripes: linear-gradient(…); }
このアプローチは、設定されているパラメーターに加えて他のプロパティを含めたい場合に最も役立ちます。 たとえば、グリッド領域を設定すると、パディングと背景を追加することもできます。
[style*='--grid-area'] { background-color: white; grid-area: var(--grid-area, auto / 1 / auto / -1); padding: 1em; }
結論
これらすべての要素をまとめ始めると、カスタムプロパティは、私たちが慣れ親しんでいる一般的な変数のユースケースをはるかに超えていることが明らかになります。 値を保存し、それらをカスケードにスコープするだけでなく、それらを使用して新しい方法でカスケードを操作し、CSSで直接よりスマートなコンポーネントを作成することができます。
このため、SMACSSやBEMなどの命名規則から、「スコープ付き」スタイルやCSS-in-JSまで、これまで依存してきたツールの多くを再考する必要があります。 これらのツールの多くは、特定性を回避したり、別の言語で動的スタイルを管理したりするのに役立ちます。これは、カスタムプロパティを使用して直接対処できるユースケースです。 JSで頻繁に計算していた動的スタイルは、生データをCSSに渡すことで処理できるようになりました。
最初は、これらの変更は「複雑さの追加」と見なされる可能性があります。これは、CSS内のロジックを確認することに慣れていないためです。 そして、すべてのコードと同様に、過剰なエンジニアリングは実際の危険になる可能性があります。 しかし、多くの場合、この力を使用して複雑さを追加するのではなく、複雑さをサードパーティのツールや規則から移動して、Webデザインのコア言語に戻し、(さらに重要なことに)ブラウザ。 スタイルに計算が必要な場合、その計算はCSS内に存在する必要があります。
これらのアイデアはすべて、さらに発展させることができます。 カスタムプロパティは広く採用され始めたばかりであり、私たちは可能なことの表面をかじり始めたばかりです。 これがどこに行くのか、そして他に人々が何を思いつくのかを見て興奮しています。 楽しむ!
参考文献
- 「CSSカスタムプロパティの使用を開始する時が来ました」SergHospodarets
- 「CSSカスタムプロパティの戦略ガイド」、Michael Riethmuller