CSS の @property
ルールは、明示的に CSS のカスタムプロパティ(CSS 変数)の構文や継承、初期値を定義できる仕組みで、2024 年 7 月 9 日リリースの Firefox 128 でサポートされたことで、主要ブラウザでのサポートが出揃いました1。
@property
を使用する目的として、大きく以下の 2 つに分けられます。
- カスタムプロパティの定義
- カスタムプロパティのアニメーション
この記事では、上記の 2 点を軸に、@property
の基本的な使い方、さらに @property
の課題について説明していきます。
前提
見出し「前提」本記事で紹介する、CSS の @property
ルールや、JS の registerProperty()
メソッドは、W3C の仕様では「CSS Properties and Values API Level 1」に属しています。
主要ブラウザではサポートされましたが、現時点では草案(Working Draft)段階のため、今後、実装方法や機能に変更が発生する可能性がある点には注意が必要です。
カスタムプロパティの定義
見出し「カスタムプロパティの定義」@property
を使用することで、カスタムプロパティの型の指定、デフォルト値の指定、継承の有無を定義することが可能になります。
以下は @property
ルールの基本的な構文です。
ここでは、カスタムプロパティ --color-accent
に対して以下の定義をしています。
- CSS のデータ型は
<color>
- 継承する(
true
) - 初期値は
hsl(180 100% 23%)
後述しますが、ここで定義したルールは、グローバルスコープになる点を覚えておきましょう。
定義したカスタムプロパティは、これまでと同じように使用します。もし、指定した値の型が syntax
で指定した型と異なる場合には initial-value
が適用されます。
なお、@property
ルールにおいて、syntax
と initial-value
の型が異なる場合には、定義自体が無効になります。
上記の例では、syntax
は <length>
ですが、initial-value
の値は 100%
なので、型は <percentage>
もしくは <length-percentage>
である必要があります。
このとき、@property
ルールのみが無効になり、カスタムプロパティ自体は有効になります。一見すると正しく動いており、見落としがちなので注意が必要です。
例えば、syntax
の値にスペルミスがあったとしても、@property
ルールが無効になるだけで、デベロッパーツールで確認しなければ気づかない可能性があります。
これらの点を踏まえて、syntax
、inherits
、initial-value
の順番で、より詳しく見ていきましょう。
syntax
この見出しのリンクsyntax
では、<length>
のような CSS のデータ型か、auto
のようなキーワード値を指定します。なお、値は引用符で囲う必要があります。
syntax
は必須なので、指定がないと @property
ルールが無効になります。
論理和(OR)
見出し「論理和(OR)」論理和(OR)は |
(VERTICAL LINE)を使用します。
以下のように、複数の型やキーワード値のいずれかに一致するように条件づけできます。
リスト
見出し「リスト」margin-block: 20px 40px
のように、値を複数指定する規則には、データ型の直後に +
(PLUS SIGN)や #
(NUMBER SIGN)を使用します。
量指定子 | 区切り |
---|---|
+ | スペース区切り |
# | カンマ区切り |
CSS のデータ型
見出し「CSS のデータ型」syntax
に指定できるデータ型は、以下のとおりです(2024 年 7 月時点)。
数が多いので圧倒されますが、最初からすべてを覚える必要はなく、必要に応じて MDN のページなどを参照するとよいでしょう。
データ型 | 内容 | 値の例 |
---|---|---|
<length> | 長さ | 100px 、1em 、50cqi |
<percentage> | パーセント値 | 50% 、-10% |
<length-percentage> | 長さ、パーセント値 | 100px 、1em 、50cqi 、50% |
<number> | 整数、小数点、指数表記 | 10 、1.6 、-50 、1e2 |
<integer> | 整数 | 10 、-50 |
<color> | カラー | blue 、#c0ffee 、oklch(40% 0.037 195) |
<image> | 二次元画像 | url('bg.jpg') 、linear-gradient(blue, white) |
<url> | url() 関数 | url('bg.jpg') |
<angle> | 角度 | 90deg 、calc(pi / 2 * 1rad) |
<time> | 時間 | 0.2s 、400ms |
<resolution> | 解像度 | 300dpi 、3dppx |
<transform-function> | 座標変換関数 | translateX(100px) 、perspective(200cqi) |
<transform-list> | 座標変換関数のリスト | translateX(100px) rotate(90deg) scale(1.2) |
<custom-ident> | ユーザ定義の識別子 | fade 、disc |
以下、使い方に気をつけたいデータ型をいくつかピックアップします。
<length-percentage>
この見出しのリンクまず、<length-percentage>
は、一見すると <length> | <percentage>
と同じようですが、calc()
関数を使用したときに違いがあります。
例えば、calc(100% - 40px)
は、<length-percentage>
では有効ですが、 <length> | <percentage>
では無効になります。
また、40px 10%
のように、長さとパーセント値が混在するときにも、<length-percentage>+
を使用する必要があります。
@property --length-percentage {
syntax: "<length-percentage>+";
inherits: false;
initial-value: 10% 10%;
}
@property --length-or-percentage {
syntax: "<length>+ | <percentage>+";
inherits: false;
initial-value: 10% 10%;
}
.foo {
/* ✅ 有効 */
--length-percentage: 40px 10%;
/* 🚫 無効 */
--length-or-percentage: 40px 10%;
}
<number>
と <integer>
この見出しのリンク<number>
と <integer>
は似ていますが、<integer>
では整数のみを受け入れるので、小数点や指数表記は無効になります。
<custom-ident>
この見出しのリンク<custom-ident>
は、例えば animation-name: fade
における fade
のように、ユーザが定義する識別子や、list-style-type
に指定する disc
や decimal
のように、ブラウザで定義されている識別子を指します。
なお、initial-value
に initial
、inherit
、unset
、revert
、revert-layer
といった CSS 全体のキーワード(CSS-wide keywords)を指定すると無効になります。
ただ、それ以外のキーワードであれば指定できるので、例えば、本来であれば <color>
に属するカラーネームの blue
を指定しても有効です。
これは、animation-name: blue
の指定が有効なことからも理解できます。
上記のような使い方は避けるべきだと考えますが、このような <custom-ident>
の型の特性を認識しておいたほうがよいでしょう。
全称構文定義(*
)
見出し「全称構文定義(」「全称構文定義」というと仰々しく感じられますが、英語表記では universal syntax definition で、すべての型を受け入れる構文定義を意味します。
CSS の全称セレクタのように *
(ASTERISK)を使用しますが、データ型同様に引用符で囲う必要があります。
なお、全称構文定義のときのみ initial-value
を省略することができます。
カスタムプロパティが評価されるタイミング
見出し「カスタムプロパティが評価されるタイミング」前回の記事「CSS の値の処理を探究する」で説明しましたが、カスタムプロパティが評価されるのは Computed Value のタイミングです。
そのため、カスタムプロパティの値に var()
関数や calc()
関数を指定した場合にも、このタイミングで参照先の値に置き換わります。
以下の例では、Computed Value 時点での --size
の値は calc(100% - 40px * 2)
となり、<length-percentage>
と型が一致するので有効な値です。
@property --size {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
.foo {
--size-1: 100%;
--size-2: 40px;
--size-3: calc(var(--size-1) - var(--size-2) * 2);
--size: var(--size-3);
inline-size: var(--size);
}
カスタムプロパティのフォールバック
見出し「カスタムプロパティのフォールバック」@property
ルールの syntax
で指定した型と、カスタムプロパティの値の型が一致しないときには、initial-value
で指定した値が適用されます。
カスケードのステップをさかのぼることも、var()
関数の第二引数に指定された代替値が使用されることもありません。
この例では、inline-size
の値に指定した --size
の型が異なるため無効になり、initial-value
で指定した 100px
が適用されます。
もし、この状態で @property
の定義自体がない場合には、IACVT と呼ばれる状態になります。その場合でも代替値が使われることはなく、inline-size: unset
が指定されたのと同じ結果になります。
var()
関数の第二引数に指定した代替値は、@property
の定義がなく、カスタムプロパティ(--size
)の定義もない場合、もしくは、--size
に明示的に initial
を指定した場合に使用されます2。
inherits
この見出しのリンクinherits
では、カスタムプロパティの継承を許可するかを指定します。なお、未定義のカスタムプロパティのデフォルト値は true
です。
@property
において inherits
の指定も必須で、指定がないとルールが無効になります3。
inherits: false
を指定した場合には、カスタムプロパティを使用できるのは対象の要素のみとなり、子孫要素では使用できません。
上記のコードでは、カスタムプロパティ --color-accent
は親要素の .foo
で指定していますが、inherits: false
を指定しているので、子要素 .bar
の color
プロパティに対して teal
は適用されずに、initial-value
で指定した blue
が適用されます。
指定自体が無効になるのではなく、initial-value
が適用される点を押さえておきましょう。
initial-value
この見出しのリンクinitial-value
では、カスタムプロパティの初期値を指定します。
前述したように、syntax
で指定した型と、initial-value
で指定した値の型が異なる場合には、@property
の定義が無効になる点に注意が必要です。
この initial-value
は、以下の場合にフォールバックとしての役割を果たします。
- カスタムプロパティを宣言せずに
var()
関数で対象のプロパティを使用した場合 @property
で指定した型と、カスタムプロパティの値の型が異なる場合@property
でinherits: false
を指定し、カスタムプロパティを子孫要素で使用した場合
以下にコード例を掲載します。
仕様において、この initial-value
は computationally independent(独立して計算可能)であることが条件とされています。
例えば、10px
や 50%
は独立して計算可能ですが、1em
といった値はそうではないので、@property
の定義自体が無効になってしまいます。
以下のように、一見問題のなさそうな指定でも、無効になってしまうので注意が必要です。
ただ、この computationally independent の線引きは曖昧で、ブラウザごとの解釈にも相違があるようです。
例えば、2024 年 7 月現在では、initial-value: 50cqi
は、Google Chrome や Safari では無効になりますが、Firefox では有効になります。
カスタムプロパティのアニメーション
見出し「カスタムプロパティのアニメーション」@property
のテクニックとしてよく取り上げられるのが、これまでは CSS では実現できなかったグラデーション(linear-gradient()
など)のアニメーションが挙げられます。
これは、カスタムプロパティの値のみでは、ブラウザがどのように値の変化を補間すればよいかを知る方法がないためですが、@property
でデータ型を明示することで、これらの値の補間が可能になりアニメーションが実現します。
以下はグラデーションにアニメーションを適用している 2 つのデモです。「PLAY」ボタンを選択すると、以下のアニメーションが開始します。
background-image
プロパティのlinear-gradient()
のアニメーションmask-image
プロパティのradial-gradient()
のアニメーション
linear-gradient()
のアニメーション
この見出しのリンク1 番目のデモの CSS を以下に抜粋します。
@property --color-stop-1 {
syntax: "<color>";
inherits: false;
initial-value: transparent;
}
@property --color-stop-2 {
syntax: "<color>";
inherits: false;
initial-value: transparent;
}
/* デモ 1 */
.demo-1 {
background-image: linear-gradient(in oklch, var(--color-stop-1), var(--color-stop-2));
animation: gradient 5s linear infinite;
}
@keyframes gradient {
0% {
--color-stop-1: oklch(74% 0.1 280);
--color-stop-2: oklch(74% 0.1 60);
}
25% {
--color-stop-1: oklch(74% 0.1 60);
--color-stop-2: oklch(74% 0.1 60);
}
50% {
--color-stop-1: oklch(74% 0.1 60);
--color-stop-2: oklch(74% 0.1 280);
}
75% {
--color-stop-1: oklch(74% 0.1 280);
--color-stop-2: oklch(74% 0.1 280);
}
100% {
--color-stop-1: oklch(74% 0.1 280);
--color-stop-2: oklch(74% 0.1 60);
}
}
このコードでは、background-image
プロパティの linear-gradient()
関数に指定している 2 つのカラー、--color-stop-1
と --color-stop-2
の値をアニメーションさせています。
さて、上記のコードで、効率やメンテナンスの観点から改善できる箇所があります。
@keyframes
で指定している oklch(74% 0.1 280)
と oklch(74% 0.1 60)
の値は繰り返し出現するので、以下のようにカスタムプロパティに格納するのが望ましいです。
/* デモ 1 */
.demo-1 {
--color-1: oklch(74% 0.1 280);
--color-2: oklch(74% 0.1 60);
background-image: linear-gradient(in oklch, var(--color-stop-1), var(--color-stop-2));
animation: gradient 5s linear infinite;
}
@keyframes gradient {
0% {
--color-stop-1: var(--color-1);
--color-stop-2: var(--color-2);
}
25% {
--color-stop-1: var(--color-2);
--color-stop-2: var(--color-2);
}
50% {
--color-stop-1: var(--color-2);
--color-stop-2: var(--color-1);
}
75% {
--color-stop-1: var(--color-1);
--color-stop-2: var(--color-1);
}
100% {
--color-stop-1: var(--color-1);
--color-stop-2: var(--color-2);
}
}
しかし、2024 年 7 月現在では、このコードでは Firefox でアニメーションの補間が有効にならず、コマ送りになってしまうので、直接値を指定する必要があるようです。
ちなみに、この Firefox の現象は、transition
にカスタムプロパティを指定しても発生しなかったので、@keyframes
もしくは animation
特有のバグのようです。
radial-gradient()
のアニメーション
この見出しのリンク続いて、2 番目のデモの CSS を抜粋します。
/* デモ 2 */
@property --color-stop-1 {
syntax: "<color>";
inherits: false;
initial-value: transparent;
}
@property --color-stop-2 {
syntax: "<color>";
inherits: false;
initial-value: transparent;
}
.demo-2 {
mask-image: radial-gradient(var(--color-stop-1), var(--color-stop-2));
animation: mask 5s linear infinite alternate;
}
@keyframes mask {
0% {
--color-stop-1: transparent;
--color-stop-2: transparent;
}
50% {
--color-stop-1: black;
--color-stop-2: transparent;
}
100% {
--color-stop-1: black;
--color-stop-2: black;
}
}
ここでは、mask-image
プロパティに対して radial-gradient()
を使用して放射状のグラデーションを適用したマスクに対してアニメーションさせています。
conic-gradient()
のアニメーション
この見出しのリンク応用例として、conic-gradient()
を使用することで、以下のような円グラフやプログレスバー風のアニメーションも実装できます。
ほかにもアイデア次第で、新たな表現が可能になりますが、カスタムプロパティのアニメーションを実装するときには、後述するアニメーション時のパフォーマンス にもご注意ください。
JavaScript で定義する方法
見出し「JavaScript で定義する方法」JS でカスタムプロパティを定義するには、registerProperty()
メソッドを使用します。
CSS.registerProperty({
name: '--prop-name',
syntax: '<color>',
inherits: false,
initialValue: 'hsl(180 100% 23%)',
});
以下の差異はありますが、CSS の @property
ルールと比較して、大きな違いはありません。
- カスタムプロパティ名を
name
に指定する syntax
の指定は必須ではなく任意で、省略すると全称構文定義(*
)になるinitialValue
の表記はキャメルケース
@property の課題
見出し「@property の課題」@property
の課題として、大きなところでは以下の 4 点が考えられます。
- ドキュメント内でグローバルスコープになる
- カスタムプロパティのアニメーションにおけるパフォーマンスの影響
- エディタのサポートが不十分
- ブラウザのサポートが不十分
順番に見ていきましょう。
スコープ
見出し「スコープ」まず、@property
はドキュメント内でグローバルスコープになります。
もし、同じ名前のカスタムプロパティに対して、複数の @property
ルールを定義した場合には、最後に定義したルールが有効になります。
@layer
ブロックを使用することで、優先順位を変えることはできます。
このスコープの課題は Shadow DOM も例外ではありません。例えば、Shadow DOM の内側で registerProperty()
メソッドを使用してカスタムプロパティを定義したとしても、Shadow DOM の境界を超えて共有されます。
以下は、Shadow DOM の内側で --color
のルールを定義していますが、Shadow DOM の外側にある <p>
要素に対してもこのルールが適用されます。そのため、color
プロパティには、initialValue
で指定している blue
が適用されます。
<!-- HTML -->
<p>Lorem ipsum</p>
<my-shadowdom></my-shadowdom>
<!-- CSS -->
<style>
@property --color {
syntax: "<color>";
inherits: true;
initial-value: teal;
}
p {
color: var(--color); /* blue */
}
</style>
<!-- JS -->
<script>
class MyShadowDOM extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const p = document.createElement('p');
p.textContent = 'Shadow DOM';
CSS.registerProperty({
name: '--color',
syntax: '<color>',
inherits: true,
initialValue: 'blue',
});
const style = document.createElement('style');
style.textContent = `
p {
color: var(--color);
}
`;
this.shadowRoot.append(style, p);
}
}
customElements.define('my-shadowdom', MyShadowDOM);
</script>
もともと、カスタムプロパティ自体が Shadow DOM の境界を越えることができるのですが、W3C のドキュメントでは、以下のようにユニークな名前をつけて競合を回避すべきと提案されています。
If a custom property is intended for private internal usage for a component, however, it is recommended that the property be given a likely-unique name, to minimize the possibility of a clash with any other context.
いささか心もとないですが、カスタムプロパティの命名規則や設計について、再考の余地が残されていると考えられます。
アニメーション時のパフォーマンス
見出し「アニメーション時のパフォーマンス」カスタムプロパティの値を transition
や animation
でアニメーションさせると、再計算や再描画が繰り返され、パフォーマンスに悪影響を及ぼすことがあります。
W3C のドキュメントでは、以下の内容が該当するようです。
Like unregistered custom properties, the value of a registered custom property can be substituted into another value with the
var()
function. However, registered custom properties substitute as their computed value, rather than the original token sequence used to produce that value.
要約すると、未登録のカスタムプロパティ同様、@property
で登録されたカスタムプロパティの値を、別の値に置き換えることが可能ですが、元の値をそのまま置き換えるのではなく、計算値(Computed Value)として置き換えられるとあります。
以下は、translate
プロパティによって、要素が水平方向に移動するだけのアニメーションのコードです。次の 2 パターンでパフォーマンスの比較をします。
- A. 直接
translate
の値を指定したアニメーション - B. カスタムプロパティの値を指定したアニメーション
/* A. 直接、値を指定したアニメーション */
.a {
animation: move-a 1s linear;
}
@keyframes move-a {
from {
translate: 0 0;
}
to {
translate: 100% 0;
}
}
/* B. カスタムプロパティのアニメーション */
@property --x {
syntax: '<length-percentage>';
inherits: false;
initial-value: 0;
}
.b {
translate: var(--x) 0;
animation: move-b 1s linear;
}
@keyframes move-b {
from {
--x: 0;
}
to {
--x: 100%;
}
}
以下は、Google Chrome のデベロッパーツールの「Performance」タブで計測した結果のスクリーンショットです。
「B. カスタムプロパティのアニメーション」では、メインスレッドで多くの処理が発生しているのがわかります。アクティビティの詳細を見るとスタイルの再計算(Recalculate Style)が頻発しており、その結果として GPU を多く使用しているようです。
カスタムプロパティのアニメーションでしか実現できない表現もあるので、一概に否定するものではありませんが、メインスレッドの負担は INP(Interaction to Next Paint)にも影響するので、これらの点も考慮して実装するのがよいでしょう。
なお、このパフォーマンスの問題は、以下の Bramus 氏のブログ記事で知りました。
エディタのサポート
見出し「エディタのサポート」@property
ルールを記述することで、使用するエディタによっては、後続するコードのシンタックスハイライトが壊れることがあります。
Visual Studio Code では、CSS ファイル(.css
)は @property
に対応しているようなのですが、ファイルの種類によっては影響を受けてしまいます。
例えば、このブログ記事は MDX 形式(.mdx
)で管理しているのですが、コードブロックに @property
ルールを含めると以下のようにシンタックスハイライトが変わってしまいます。
@prorperty
を追加すると、後続するコードのシンタックスハイライトが変わるこの問題は、拡張機能に原因があると考えられそうですが、HTML ファイル(.html
)内のスタイルシートで @property
を定義したときも、同様に後続のコードに影響を及ぼします。エディタ全般において、まだサポートが十分ではないといえそうです4。
ブラウザのサポート
見出し「ブラウザのサポート」前述の「カスタムプロパティのアニメーション」にて、Firefox でアニメーションの補間が適用されない例を取り上げました。
エディタ同様に、ブラウザにおける @property
のサポートも安定しているとはいえないので、特にカスタムプロパティのアニメーションを実装したときには、各種ブラウザ検証は少し手厚くしたほうがよいかもしれません。
おわりに
見出し「おわりに」この記事では、@property
の基本的な使い方や課題について説明しました。
カスタムプロパティを @property
ルールで定義することで、より堅牢なコードになることに加え、カスタムプロパティのアニメーションが可能になり、CSS における表現の可能性が広がることが期待できます。
一方で、@property
がグローバルスコープであるため、またしてもスコープの管理や命名規則に苦慮しなければならない点には、若干うんざりしてしまいます。
加えて、カスタムプロパティのアニメーションがパフォーマンスへの負担が大きいことを知ると、気軽に手を出しづらく感じてしまうのも事実です。
今回の考察を踏まえて、どのカスタムプロパティに対して @property
を定義するのか、命名規則をどうするか、どこで定義するのかといった、設計面でのポイントを引き続き考えていきたいです。
参考文献
見出し「参考文献」本記事の作成にあたり、以下のウェブページを参考にしました。
- @property | MDN(外部リンクを開く)
- CSS data types | MDN(外部リンクを開く)
- CSS Properties and Values API | MDN(外部リンクを開く)
- CSS Properties and Values API Level 1 | W3C(外部リンクを開く)
- CSS Values and Units Module Level 4 | W3C(外部リンクを開く)
- The Times You Need A Custom @property Instead Of A CSS Variable | Smashing Magazine(外部リンクを開く)
- Type safe CSS design systems with @property | Adam Argyle(外部リンクを開く)
- @property: giving superpowers to CSS variables | web.dev(外部リンクを開く)
- The gotcha with @property animating custom properties | Bram.us(外部リンクを開く)
- Providing Type Definitions for CSS with @property | Modern CSS Solutions(外部リンクを開く)
脚注
-
Safari での
@property
のサポートは 16.4 からです。 ↩ -
この
initial
値は「Guaranteed-Invalid Value」と呼ばれ、「Space Toggle」と呼ばれるテクニックで用いられることがあります。 ↩ -
inherits
を毎回指定するのは面倒なので、省略したらtrue
でよいのでは、と思ってしまいますが、そういうわけにはいかないようです。 ↩ -
さらにいえば、本記事に掲載しているコードブロックも、VS Code と同じエンジンの Shiki を使用しているため、
@property
ルールに後続するコードのシンタックスハイライトが一部壊れています。 ↩